Visuelle Bewertung und Anpassung des Handels im MetaTrader 5
Inhalt
- Einführung
- Speichern der Handelshistorie in einer Datei
- Analyse der Handelshistorie aus der Datei im Tester
- Anpassung von Stop-Aufträgen
- Schlussfolgerung
Einführung
Stellen wir uns eine Situation vor: Auf einem Konto wird seit geraumer Zeit mehr oder weniger aktiv mit verschiedenen Instrumenten gehandelt, wobei verschiedene EAs und in einigen Fällen sogar manuell eingesetzt werden. Und jetzt, nach einiger Zeit, wollen wir die Ergebnisse all dieser Arbeit sehen. Natürlich können wir die Standard-Handelsberichte im Terminal durch Drücken der Tastenkombination Alt+E einsehen. Wir können auch Handelssymbole in unser Diagramm laden und Einstiegs- und Ausstiegszeiten für unsere Positionen sehen. Was aber, wenn wir die Dynamik unseres Handels sehen wollen, wo und wie Positionen eröffnet und geschlossen wurden? Wir können uns jedes Symbol einzeln oder alle auf einmal ansehen, einschließlich der Eröffnung und Schließung von Positionen, der Niveaus, auf denen Stop-Orders platziert wurden, und ob ihre Größe gerechtfertigt war. Wie wäre es, wenn wir uns die Frage stellen: „Was wäre passiert, wenn...“ (und hier gibt es viele Möglichkeiten – verschiedene Stopps, die Verwendung verschiedener Algorithmen und Kriterien, die Verwendung von Trailing-Positionen oder das Verschieben von Stopps zum Breakeven usw.) und dann alle unsere „Wenns“ mit einem klaren, sichtbaren Ergebnis testen. Wie könnte sich der Handel verändern, wenn...
Es stellt sich heraus, dass bereits alles vorhanden ist, um solche Probleme zu lösen. Alles, was wir tun müssen, ist, die Kontohistorie in eine Datei zu laden – alle abgeschlossenen Handelsgeschäfte – und dann einen EA im Strategietester laufen zu lassen, der Handelsgeschäfte aus der Datei liest und Positionen im Strategietester des Client-Terminals öffnet/schließt. Mit einem solchen EA können wir Code hinzufügen, um die Bedingungen für das Verlassen von Positionen zu ändern und zu vergleichen, wie sich der Handel verändert, und was passiert wäre, wenn...
Wie kann das für uns von Nutzen sein? Ein weiteres Instrument, um die besten Ergebnisse zu finden und Anpassungen am Handel vorzunehmen, der bereits seit einiger Zeit auf einem Konto läuft. Die visuelle Prüfung ermöglicht es uns, dynamisch zu sehen, ob die Positionen für ein bestimmtes Instrument korrekt geöffnet wurden, ob sie zum richtigen Zeitpunkt geschlossen wurden, usw. Und was am wichtigsten ist: Ein neuer Algorithmus kann einfach zum EA-Code hinzugefügt, getestet, die Ergebnisse ermittelt und die EAs, die auf diesem Konto arbeiten, angepasst werden.
Lassen Sie uns die folgende Logik für das Verhalten des EA erstellen:
- Wenn der EA auf dem Chart eines beliebigen Instruments gestartet wird, sammelt er die gesamte Historie der Handelsgeschäfte auf dem aktuellen Konto, speichert alle Handelsgeschäfte in einer Datei und tut dann – nichts;
- Wenn der EA im Tester gestartet wird, liest er die in der Datei aufgezeichnete Handelshistorie und wiederholt während des Tests alle Handelsgeschäfte aus der Datei mit Eröffnen und Schließen der Positionen.
So bereitet der EA zunächst eine Datei mit der Handelshistorie vor (wenn er auf einem Chart ausgeführt wird) und führt dann Handelsgeschäfte aus der Datei aus, wobei er den Handel auf dem Konto vollständig wiederholt (wenn er im Strategietester ausgeführt wird).
Als Nächstes werden wir Änderungen am EA vornehmen, um unterschiedliche StopLoss- und TakeProfit-Werte für die im Tester geöffneten Positionen festlegen zu können.
Speichern der Handelshistorie in einer Datei
Im Terminalverzeichnis \MQL5\Experts\ erstellen wir einen neuen Ordner TradingByHistoryDeals, der eine neue EA-Datei namens TradingByHistoryDeals.mq5 enthält.
Der EA sollte die Möglichkeit haben, auszuwählen, welches Symbol und welche magische Zahl getestet werden soll. Wenn mehrere EAs für mehrere Symbole oder magische Zahlen auf dem Konto arbeiten, können wir in den Einstellungen auswählen, an welchem Symbol oder welcher magischen Zahl wir interessiert sind (oder an allen gleichzeitig).
//+------------------------------------------------------------------+ //| TradingByHistoryDeals.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */
Die Standardwerte für das Symbol und die magische Zahl sind der leere String und -1. Mit diesen Werten sortiert der EA den Handelsverlauf nicht nach Symbolen oder magischen Zahlen – es wird der gesamte Handelsverlauf getestet. Die dritte Zeichenfolge weist den EA an, Beschreibungen aller in der Datei gespeicherten Handelsgeschäfte in das Journal auszugeben (oder nicht), damit wir die Richtigkeit der gespeicherten Daten eindeutig überprüfen können.
Jedes Handelsgeschäft besteht aus einer ganzen Reihe verschiedener Parameter, die durch unterschiedliche Handelsgeschäftseigenschaften beschrieben werden. Am einfachsten ist es, alle Handelsgeschäftseigenschaften in einer Struktur aufzuschreiben. Um eine große Anzahl von Handelsgeschäften in eine Datei zu schreiben, ist es notwendig, ein Array von Strukturen zu verwenden. Dann speichern wir dieses Array in der Datei. Die MQL5-Sprache hat alles, was man dazu braucht. Die Logik für die Speicherung des Handelsgeschäftsverlaufs in einer Datei sieht folgendermaßen aus:
- sich in einer Schleife durch die historischen Handelsgeschäfte bewegen;
- Empfang des nächsten Handelsgeschäfts und Zuweisung von dessen Daten in die Struktur;
- Speichern der erstellten Handelsgeschäftsstruktur im Handelsgeschäftsfeld;
- Am Ende der Schleife wird das vorbereitete Array von Strukturen in der Datei gespeichert.
Alle zusätzlichen Codes – Strukturen, Klassen, Enumerationen – werden in eine separate Datei geschrieben. Benennen wir sie nach der zukünftigen Klasse des Symbolhandelsobjekts.
Erstellen wir im selben Ordner eine neue eingebundene Datei namens SymbolTrade.mqh.
Wir implementieren die Makro-Substitutionen für den Namen des Ordners, der die Verlaufsdatei enthalten soll, den Dateinamen und den Dateipfad und fügen wir alle erforderlichen Standardbibliotheksdateien hinzu:
//+------------------------------------------------------------------+ //| SymbolTrade.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define DIRECTORY "TradingByHistoryDeals" #define FILE_NAME "HistoryDealsData.bin" #define PATH DIRECTORY+"\\"+FILE_NAME #include <Arrays\ArrayObj.mqh> #include <Trade\Trade.mqh>
Als Nächstes werden wir die Struktur für die Handelsgeschäfte bzw. Deals schreiben:
//+------------------------------------------------------------------+ //| Deal structure. Used to create a deal history file | //+------------------------------------------------------------------+ struct SDeal { ulong ticket; // Deal ticket long order; // Order the deal is based on long pos_id; // Position ID long time_msc; // Time in milliseconds datetime time; // Time double volume; // Volume double price; // Price double profit; // Profit double commission; // Deal commission double swap; // Accumulated swap at closing double fee; // Payment for the deal is accrued immediately after the deal is completed double sl; // Stop Loss level double tp; // Take Profit level ENUM_DEAL_TYPE type; // Type ENUM_DEAL_ENTRY entry; // Position change method ENUM_DEAL_REASON reason; // Deal reason or source long magic; // EA ID int digits; // Symbol digits ushort symbol[16]; // Symbol ushort comment[64]; // Deal comment ushort external_id[256]; // Deal ID in an external trading system (on the exchange) //--- Set string properties bool SetSymbol(const string deal_symbol) { return(::StringToShortArray(deal_symbol, symbol)==deal_symbol.Length()); } bool SetComment(const string deal_comment) { return(::StringToShortArray(deal_comment, comment)==deal_comment.Length()); } bool SetExternalID(const string deal_external_id) { return(::StringToShortArray(deal_external_id, external_id)==deal_external_id.Length()); } //--- Return string properties string Symbol(void) { return(::ShortArrayToString(symbol)); } string Comment(void) { return(::ShortArrayToString(comment)); } string ExternalID(void) { return(::ShortArrayToString(external_id)); } };
Da wir die Deal-Strukturen in einer Datei speichern werden und nur einfache Typ-Strukturen in eine Datei geschrieben werden können (siehe FileWriteArray()), sollten alle String-Variablen durch ushort-Arrays ersetzt und Methoden zum Schreiben und Zurückgeben von Struktur-String-Eigenschaften erstellt werden.
Die erstellte Struktur ist nur notwendig, um den Handelsgeschäftsverlauf in einer Datei zu speichern und den aufgezeichneten Verlauf aus der Datei zu lesen. Im EA selbst wird eine Liste von Objekten erstellt, in der die Objekte der Deal-Klasse gespeichert werden. Um das gewünschte Handelsgeschäft in der Liste zu suchen und das Array zu sortieren, müssen wir die Eigenschaft des Handelsgeschäfts angeben, nach der es in der Liste gesucht werden soll. Um nach einem Handelsgeschäft zu suchen, sollte die Liste der Objekte nach der gewünschten Eigenschaft sortiert werden.
Schreiben wir eine Liste aller Eigenschaften des Handelsgeschäftsobjekts, anhand derer wir eine Suche durchführen können:
//--- Deal sorting types enum ENUM_DEAL_SORT_MODE { SORT_MODE_DEAL_TICKET = 0, // Mode of comparing/sorting by a deal ticket SORT_MODE_DEAL_ORDER, // Mode of comparing/sorting by the order a deal is based on SORT_MODE_DEAL_TIME, // Mode of comparing/sorting by a deal time SORT_MODE_DEAL_TIME_MSC, // Mode of comparing/sorting by a deal time in milliseconds SORT_MODE_DEAL_TYPE, // Mode of comparing/sorting by a deal type SORT_MODE_DEAL_ENTRY, // Mode of comparing/sorting by a deal direction SORT_MODE_DEAL_MAGIC, // Mode of comparing/sorting by a deal magic number SORT_MODE_DEAL_REASON, // Mode of comparing/sorting by a deal reason or source SORT_MODE_DEAL_POSITION_ID, // Mode of comparing/sorting by a position ID SORT_MODE_DEAL_VOLUME, // Mode of comparing/sorting by a deal volume SORT_MODE_DEAL_PRICE, // Mode of comparing/sorting by a deal price SORT_MODE_DEAL_COMMISSION, // Mode of comparing/sorting by commission SORT_MODE_DEAL_SWAP, // Mode of comparing/sorting by accumulated swap on close SORT_MODE_DEAL_PROFIT, // Mode of comparing/sorting by a deal financial result SORT_MODE_DEAL_FEE, // Mode of comparing/sorting by a deal fee SORT_MODE_DEAL_SL, // Mode of comparing/sorting by Stop Loss level SORT_MODE_DEAL_TP, // Mode of comparing/sorting by Take Profit level SORT_MODE_DEAL_SYMBOL, // Mode of comparing/sorting by a name of a traded symbol SORT_MODE_DEAL_COMMENT, // Mode of comparing/sorting by a deal comment SORT_MODE_DEAL_EXTERNAL_ID, // Mode of comparing/sorting by a deal ID in an external trading system SORT_MODE_DEAL_TICKET_TESTER, // Mode of comparing/sorting by a deal ticket in the tester SORT_MODE_DEAL_POS_ID_TESTER, // Mode of comparing/sorting by a position ID in the tester };
Hier gibt es zusätzlich zu den Standardeigenschaften des Handelsgeschäfts zwei weitere: das Handelsgeschäftsticket im Tester und die Positions-ID im Tester. Der Punkt ist, dass wir im Tester auf der Grundlage von Daten aus realen Handelsgeschäften handeln werden, während im Tester eröffnete Positionen und dementsprechend ihre entsprechenden Handelsgeschäfte ein völlig anderes Ticket und eine andere ID im Tester haben. Um ein echtes Handelsgeschäft mit einem Handelsgeschäft im Tester (sowie der ID) vergleichen zu können, müssen wir das Ticket und die Positions-ID im Tester in den Eigenschaften des Handelsgeschäftsobjekts speichern und dann anhand dieser gespeicherten Daten das Handelsgeschäft im Tester mit dem echten Handelsgeschäft im Verlauf vergleichen.
Lassen wir diese Datei erst einmal beiseite und gehen wir zu der EA-Datei über, die wir vorhin erstellt haben. Fügen wir das Array der Strukturen hinzu, in das wir die Strukturen aller Handelsgeschäfte in der Historie einfügen werden:
//--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ //--- global variables SDeal ExtArrayDeals[]={};
Und wir werden Funktionen für die Arbeit mit historischen Handelsgeschäften schreiben.
Funktion, die den Handelsgeschäftsverlauf in einem Array speichert:
//+------------------------------------------------------------------+ //| Save deals from history into the array | //+------------------------------------------------------------------+ int SaveDealsToArray(SDeal &array[], bool logs=false) { //--- deal structure SDeal deal={}; //--- request the deal history in the interval from the very beginning to the current moment if(!HistorySelect(0, TimeCurrent())) { Print("HistorySelect() failed. Error ", GetLastError()); return 0; } //--- total number of deals in the list int total=HistoryDealsTotal(); //--- handle each deal for(int i=0; i<total; i++) { //--- get the ticket of the next deal (the deal is automatically selected to get its properties) ulong ticket=HistoryDealGetTicket(i); if(ticket==0) continue; //--- save only balance and trading deals ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE); if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE) continue; //--- save the deal properties in the structure deal.ticket=ticket; deal.type=deal_type; deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER); deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY); deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON); deal.time=(datetime)HistoryDealGetInteger(ticket, DEAL_TIME); deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC); deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME); deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE); deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT); deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION); deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP); deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE); deal.sl=HistoryDealGetDouble(ticket, DEAL_SL); deal.tp=HistoryDealGetDouble(ticket, DEAL_TP); deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC); deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL)); deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT)); deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID)); deal.digits=(int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS); //--- increase the array and int size=(int)array.Size(); ResetLastError(); if(ArrayResize(array, size+1, total)!=size+1) { Print("ArrayResize() failed. Error ", GetLastError()); continue; } //--- save the deal in the array array[size]=deal; //--- if allowed, display the description of the saved deal to the journal if(logs) DealPrint(deal, i); } //--- return the number of deals stored in the array return (int)array.Size(); }
Der Funktionscode ist ausführlich kommentiert. Wir wählen die gesamte Handelshistorie vom Anfang bis zum aktuellen Zeitpunkt aus, holen uns jedes aufeinanderfolgende historische Handelsgeschäft, speichern dessen Eigenschaften in den Strukturfeldern und weisen die Strukturvariable einem Array zu. Am Ende der Schleife durch die Handelsgeschäftshistorie geben wir die Größe des resultierenden Arrays von Handelsgeschäften zurück. Um den Fortschritt der Erfassung von Handelsgeschäften in einem Array zu überwachen, können wir jedes bearbeitete Handelsgeschäft im Journal ausdrucken. Dazu müssen wir beim Aufruf der Funktion das Flag logs auf true setzen.
Die Funktion, die alle Handelsgeschäfte aus einem Array von Handelsgeschäften in das Journal druckt:
//+------------------------------------------------------------------+ //| Display deals from the array to the journal | //+------------------------------------------------------------------+ void DealsArrayPrint(SDeal &array[]) { int total=(int)array.Size(); //--- if an empty array is passed, report this and return 'false' if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return; } //--- In a loop through the deal array, print out a description of each deal for(int i=0; i<total; i++) { DealPrint(array[i], i); } }
Wir wollen mehrere Funktionen implementieren, um die Handelsgeschäftsbeschreibung im Journal anzuzeigen.
Die Funktion, die eine Beschreibung der Transaktionsart zurückgibt:
//+------------------------------------------------------------------+ //| Return the deal type description | //+------------------------------------------------------------------+ string DealTypeDescription(const ENUM_DEAL_TYPE type) { switch(type) { case DEAL_TYPE_BUY : return "Buy"; case DEAL_TYPE_SELL : return "Sell"; case DEAL_TYPE_BALANCE : return "Balance"; case DEAL_TYPE_CREDIT : return "Credit"; case DEAL_TYPE_CHARGE : return "Additional charge"; case DEAL_TYPE_CORRECTION : return "Correction"; case DEAL_TYPE_BONUS : return "Bonus"; case DEAL_TYPE_COMMISSION : return "Additional commission"; case DEAL_TYPE_COMMISSION_DAILY : return "Daily commission"; case DEAL_TYPE_COMMISSION_MONTHLY : return "Monthly commission"; case DEAL_TYPE_COMMISSION_AGENT_DAILY : return "Daily agent commission"; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: return "Monthly agent commission"; case DEAL_TYPE_INTEREST : return "Interest rate"; case DEAL_TYPE_BUY_CANCELED : return "Canceled buy deal"; case DEAL_TYPE_SELL_CANCELED : return "Canceled sell deal"; case DEAL_DIVIDEND : return "Dividend operations"; case DEAL_DIVIDEND_FRANKED : return "Franked (non-taxable) dividend operations"; case DEAL_TAX : return "Tax charges"; default : return "Unknown deal type: "+(string)type; } }
Je nach der an die Funktion übergebenen Handelsgeschäftsart wird die entsprechende Zeichenfolge angezeigt.
Die Funktion retourniert die Beschreibung der Positionsänderungsmethode:
//+------------------------------------------------------------------+ //| Return position change method | //+------------------------------------------------------------------+ string DealEntryDescription(const ENUM_DEAL_ENTRY entry) { switch(entry) { case DEAL_ENTRY_IN : return "Entry In"; case DEAL_ENTRY_OUT : return "Entry Out"; case DEAL_ENTRY_INOUT : return "Entry InOut"; case DEAL_ENTRY_OUT_BY : return "Entry OutBy"; default : return "Unknown entry: "+(string)entry; } }
Je nach der Methode zur Änderung der Position, die der Funktion übergeben wird, wird die entsprechende Zeichenfolge angezeigt.
Die Funktion, die eine Beschreibung eines Handelsgeschäfts zurückgibt:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string DealDescription(SDeal &deal, const int index) { string indexs=StringFormat("% 5d", index); if(deal.type!=DEAL_TYPE_BALANCE) return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f", indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type), deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price, TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp)); else return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s", indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type), deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time))); }
Handelt es sich um ein Saldengeschäft, so wird eine Beschreibung in folgender Form angezeigt:
0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48
Andernfalls wird die Handelsgeschäftsbeschreibung in einem anderen Format angezeigt:
1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983
Die Funktion, die eine Handelsgeschäftsbeschreibung in das Journal druckt:
//+------------------------------------------------------------------+ //| Print deal data in the journal | //+------------------------------------------------------------------+ void DealPrint(SDeal &deal, const int index) { Print(DealDescription(deal, index)); }
Hier ist alles klar – wir drucken einfach die Zeichenkette aus, die wir mit der Funktion DealDescription() erhalten haben.
Wir schreiben Funktionen zum Schreiben und Lesen eines Arrays der Deals in/aus einer Datei.
Die Funktion, die eine Datei zum Schreiben öffnet:
//+------------------------------------------------------------------+ //| Open a file for writing, return a handle | //+------------------------------------------------------------------+ bool FileOpenToWrite(int &handle) { ResetLastError(); handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) { PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError()); return false; } //--- successful return true; }
Die Funktion öffnet eine Datei zum Lesen:
//+------------------------------------------------------------------+ //| Open a file for reading, return a handle | //+------------------------------------------------------------------+ bool FileOpenToRead(int &handle) { ResetLastError(); handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) { PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError()); return false; } //--- successful return true; }
Die Funktionen öffnen eine Datei zum Lesen bzw. Schreiben. In den formalen Parametern wird als Referenz eine Variable übergeben, der das Datei-Handle zugewiesen wurde. Es wird true zurückgegeben, wenn die Datei erfolgreich geöffnet wurde, und false, wenn ein Fehler aufgetreten ist.
Die Funktion, die Deal-Daten aus einem Array in der Datei speichert:
//+------------------------------------------------------------------+ //| Save deal data from the array to the file | //+------------------------------------------------------------------+ bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size) { //--- if an empty array is passed, report this and return 'false' if(array.Size()==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- open the file for writing, get its handle int handle=INVALID_HANDLE; if(!FileOpenToWrite(handle)) return false; //--- move the file pointer to the end of the file bool res=true; ResetLastError(); res&=FileSeek(handle, 0, SEEK_END); if(!res) PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError()); //--- write the array data to the end of the file file_size=0; res&=(FileWriteArray(handle, array)==array.Size()); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- close the file FileClose(handle); return res; }
Die Funktion erhält ein Array von Strukturen, die in einer Datei gespeichert werden müssen. Die Variable, die die Größe der erstellten Datei erhält, wird in den formalen Parametern der Funktion als Referenz übergeben. Wir öffnen die Datei, verschieben den Dateizeiger an das Ende der Datei und schreiben Daten aus dem Array von Strukturen in die Datei, beginnend mit dem Zeiger. Wenn der Schreibvorgang abgeschlossen ist, wird die Datei geschlossen.
Nachdem das Array der Handelsgeschäftsstrukturen in der Datei gespeichert wurde, können alle Handelsgeschäfte aus dieser Datei in das Array zurückgelesen werden, um dann Listen von Handelsgeschäften zu erstellen und mit ihnen im Tester zu arbeiten.
Die Funktion, die die Handelsgeschäftsdaten aus der Datei in das Array lädt:
//+------------------------------------------------------------------+ //| Load the deal data from the file into the array | //+------------------------------------------------------------------+ bool FileReadDealsToArray(SDeal &array[], ulong &file_size) { //--- open the file for reading, get its handle int handle=INVALID_HANDLE; if(!FileOpenToRead(handle)) return false; //--- move the file pointer to the end of the file bool res=true; ResetLastError(); //--- read data from the file into the array file_size=0; res=(FileReadArray(handle, array)>0); if(!res) PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError()); else file_size=FileSize(handle); //--- close the file FileClose(handle); return res; }
Auf der Grundlage der oben erstellten Funktionen werden wir eine Funktion schreiben, die den Verlauf der Handelsgeschäfte liest und in die Datei schreibt.
Die Funktion, die eine Datei mit historischen Handelsgeschäften vorbereitet:
//+------------------------------------------------------------------+ //| Prepare a file with history deals | //+------------------------------------------------------------------+ bool PreparesDealsHistoryFile(SDeal &deals_array[]) { //--- save all the account deals in the deal array int total=SaveDealsToArray(deals_array); if(total==0) return false; //--- write the deal array data to the file ulong file_size=0; if(!FileWriteDealsFromArray(deals_array, file_size)) return false; //--- print in the journal how many deals were read and saved to the file, the path to the file and its size PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size", deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size); //--- now, to perform a check, we will read the data from the file into the array ArrayResize(deals_array, 0, total); if(!FileReadDealsToArray(deals_array, file_size)) return false; //--- print in the journal how many bytes were read from the file and the number of deals received in the array PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size()); return true; }
Die Kommentare im Code machen die Logik deutlich. Die Funktion wird in OnInit() gestartet und bereitet die Datei mit den Handelsgeschäften für die weitere Arbeit vor:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- If the EA is not running in the tester if(!MQLInfoInteger(MQL_TESTER)) { //--- prepare a file with all historical deals if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- print all deals in the journal after loading them from the file if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- get the first balance deal, create the message text and display it using Alert SDeal deal=ExtArrayDeals[0]; long leverage=AccountInfoInteger(ACCOUNT_LEVERAGE); double start_money=deal.profit; datetime first_time=deal.time; string start_time=TimeToString(deal.time, TIME_DATE); string message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage); //--- notify via alert of the recommended parameters of the strategy tester for starting the test Alert(message); } //--- All is successful return(INIT_SUCCEEDED); }
Zusätzlich zur Speicherung aller historischen Abschlüsse in einer Datei wird eine Warnung mit einer Meldung über die empfohlenen Einstellungen des Testers angezeigt – Anfangssaldo, Hebel und die Startzeit des Tests, die dem Datum des ersten Abschlusses entspricht. Sie könnte zum Beispiel so aussehen:
Alert: Now you can run testing Interval: 2024.09.13 - current date Initial deposit: 3000.00, leverage 1:500
Diese Einstellungen des Testers führen dazu, dass das Endergebnis im Tester dem tatsächlichen Ergebnis am nächsten kommt.
Die Struktur des Handelsgeschäftssatzes in der Datei \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh ist nur zum Speichern des Handelsgeschäfts in der Datei und zum Lesen der gespeicherten Historie aus der Datei gedacht. Um unsere Arbeit fortzusetzen, müssen wir eine Klasse der Deals erstellen, deren Objekte in Listen gespeichert werden sollen. Die Listen selbst werden in den Objekten der Handelsklasse für den Tester gespeichert. Handelsobjekte wiederum sind ebenfalls Klassenobjekte, die in einer eigenen Liste gespeichert werden. Jedes Handelsobjekt wird durch seine Zugehörigkeit zu einem bestimmten Symbol bestimmt – die Anzahl der am Handel beteiligten Symbole bestimmt die Anzahl der Handelsobjekte. Die Handelsobjekte selbst enthalten eine Liste von Transaktionen nur für ihr Symbol und ihre eigenen Objekte der Klasse CTrade aus der Standardbibliothek. Dadurch kann jedes Handelsobjekt der Klasse CTrade an die Bedingungen des gehandelten Symbols angepasst werden.
Schreiben wir eine Handelsklasse in die Datei \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh.
//+------------------------------------------------------------------+ //| Deal class. Used for trading in the strategy tester | //+------------------------------------------------------------------+ class CDeal : public CObject { protected: //--- Integer properties ulong m_ticket; // Deal ticket. Unique number assigned to each deal long m_order; // Deal order number datetime m_time; // Deal execution time long m_time_msc; // Deal execution time in milliseconds since 01.01.1970 ENUM_DEAL_TYPE m_type; // Deal type ENUM_DEAL_ENTRY m_entry; // Deal entry - entry in, entry out, reverse long m_magic; // Magic number for a deal (see ORDER_MAGIC) ENUM_DEAL_REASON m_reason; // Deal execution reason or source long m_pos_id; // The ID of the position opened, modified or closed by the deal //--- Real properties double m_volume; // Deal volume double m_price; // Deal price double m_commission; // Deal commission double m_swap; // Accumulated swap when closing double m_profit; // Deal financial result double m_fee; // Fee for making a deal charged immediately after performing a deal double m_sl; // Stop Loss level double m_tp; // Take Profit level //--- String properties string m_symbol; // Name of the symbol for which the deal is executed string m_comment; // Deal comment string m_external_id; // Deal ID in an external trading system (on the exchange) //--- Additional properties int m_digits; // Symbol digits double m_point; // Symbol point ulong m_ticket_tester; // Position ticket in the tester long m_pos_id_tester; // Position ID in the tester public: //--- Set deal propertie void SetTicket(const ulong ticket) { this.m_ticket=ticket; } void SetOrder(const long order) { this.m_order=order; } void SetTime(const datetime time) { this.m_time=time; } void SetTimeMsc(const long value) { this.m_time_msc=value; } void SetType(const ENUM_DEAL_TYPE type) { this.m_type=type; } void SetEntry(const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } void SetMagic(const long magic) { this.m_magic=magic; } void SetReason(const ENUM_DEAL_REASON reason) { this.m_reason=reason; } void SetPositionID(const long id) { this.m_pos_id=id; } void SetVolume(const double volume) { this.m_volume=volume; } void SetPrice(const double price) { this.m_price=price; } void SetCommission(const double commission) { this.m_commission=commission; } void SetSwap(const double swap) { this.m_swap=swap; } void SetProfit(const double profit) { this.m_profit=profit; } void SetFee(const double fee) { this.m_fee=fee; } void SetSL(const double sl) { this.m_sl=sl; } void SetTP(const double tp) { this.m_tp=tp; } void SetSymbol(const string symbol) { this.m_symbol=symbol; } void SetComment(const string comment) { this.m_comment=comment; } void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } void SetTicketTester(const ulong ticket) { this.m_ticket_tester=ticket; } void SetPosIDTester(const long pos_id) { this.m_pos_id_tester=pos_id; } //--- Return deal properties ulong Ticket(void) const { return this.m_ticket; } long Order(void) const { return this.m_order; } datetime Time(void) const { return this.m_time; } long TimeMsc(void) const { return this.m_time_msc; } ENUM_DEAL_TYPE TypeDeal(void) const { return this.m_type; } ENUM_DEAL_ENTRY Entry(void) const { return this.m_entry; } long Magic(void) const { return this.m_magic; } ENUM_DEAL_REASON Reason(void) const { return this.m_reason; } long PositionID(void) const { return this.m_pos_id; } double Volume(void) const { return this.m_volume; } double Price(void) const { return this.m_price; } double Commission(void) const { return this.m_commission; } double Swap(void) const { return this.m_swap; } double Profit(void) const { return this.m_profit; } double Fee(void) const { return this.m_fee; } double SL(void) const { return this.m_sl; } double TP(void) const { return this.m_tp; } string Symbol(void) const { return this.m_symbol; } string Comment(void) const { return this.m_comment; } string ExternalID(void) const { return this.m_external_id; } int Digits(void) const { return this.m_digits; } double Point(void) const { return this.m_point; } ulong TicketTester(void) const { return this.m_ticket_tester; } long PosIDTester(void) const { return this.m_pos_id_tester; } //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const { const CDeal *obj=node; switch(mode) { case SORT_MODE_DEAL_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_DEAL_ORDER : return(this.Order() > obj.Order() ? 1 : this.Order() < obj.Order() ? -1 : 0); case SORT_MODE_DEAL_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_DEAL_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_DEAL_TYPE : return(this.TypeDeal() > obj.TypeDeal() ? 1 : this.TypeDeal() < obj.TypeDeal() ? -1 : 0); case SORT_MODE_DEAL_ENTRY : return(this.Entry() > obj.Entry() ? 1 : this.Entry() < obj.Entry() ? -1 : 0); case SORT_MODE_DEAL_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_DEAL_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_DEAL_POSITION_ID : return(this.PositionID() > obj.PositionID() ? 1 : this.PositionID() < obj.PositionID() ? -1 : 0); case SORT_MODE_DEAL_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_DEAL_PRICE : return(this.Price() > obj.Price() ? 1 : this.Price() < obj.Price() ? -1 : 0); case SORT_MODE_DEAL_COMMISSION : return(this.Commission() > obj.Commission() ? 1 : this.Commission() < obj.Commission() ? -1 : 0); case SORT_MODE_DEAL_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_DEAL_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_DEAL_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case SORT_MODE_DEAL_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_DEAL_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_DEAL_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_DEAL_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_DEAL_EXTERNAL_ID : return(this.ExternalID() >obj.ExternalID() ? 1 : this.ExternalID() <obj.ExternalID() ? -1 : 0); case SORT_MODE_DEAL_TICKET_TESTER : return(this.TicketTester()>obj.TicketTester()? 1 : this.TicketTester()<obj.TicketTester() ? -1 : 0); case SORT_MODE_DEAL_POS_ID_TESTER : return(this.PosIDTester() >obj.PosIDTester() ? 1 : this.PosIDTester() <obj.PosIDTester() ? -1 : 0); default : return(WRONG_VALUE); } } //--- Constructors/destructor CDeal(const ulong ticket, const string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(0), m_pos_id_tester(0) { this.m_digits=(int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); } CDeal(void) {} ~CDeal(void) {} };
Die Klasse wiederholt fast vollständig die zuvor erstellte Handelsgeschäftsstruktur. Zusätzlich zu den Handelsgeschäftseigenschaften wurden weitere Eigenschaften hinzugefügt – Ziffern und Punkt des Symbols, für das das Handelsgeschäft getätigt wurde. Dies vereinfacht die Ausgabe der Handelsgeschäftsbeschreibung, da diese Daten sofort bei der Erstellung des Objekts im Konstruktor des Handelsgeschäfts festgelegt werden, wodurch die Notwendigkeit entfällt, diese Eigenschaften für jedes Handelsgeschäft (falls sie benötigt werden) beim Zugriff darauf zu ermitteln.
Außerdem wird hier eine virtuelle Methode Compare() für den Vergleich von zwei Handelsgeschäftsobjekten erstellt – sie wird beim Sortieren der Listen von Handelsgeschäften verwendet, um das gewünschte Handelsgeschäft anhand der angegebenen Eigenschaft zu finden.
Lassen Sie uns nun eine Handelssymbolklasse erstellen. Die Klasse speichert eine Liste der Handelsgeschäfte, die unter Verwendung des in den Objekteigenschaften eingestellten Symbols durchgeführt wurden, und der Tester fordert diese Handelsgeschäfte zum Kopieren an. Im Allgemeinen wird diese Klasse die Grundlage für das Kopieren von Handelsgeschäften sein, die auf einem Konto nach Symbolen im Strategietester getätigt werden:
//+------------------------------------------------------------------+ //| Class for trading by symbol | //+------------------------------------------------------------------+ CDeal DealTmp; // Temporary deal object for searching by properties class CSymbolTrade : public CObject { private: int m_index_next_deal; // Index of the next deal that has not yet been handled int m_deals_processed; // Number of handled deals protected: MqlTick m_tick; // Tick structure CArrayObj m_list_deals; // List of deals carried out by symbol CTrade m_trade; // Trading class string m_symbol; // Symbol name public: //--- Return the list of deals CArrayObj *GetListDeals(void) { return(&this.m_list_deals); } //--- Set a symbol void SetSymbol(const string symbol) { this.m_symbol=symbol; } //--- (1) Set and (2) returns the number of handled deals void SetNumProcessedDeals(const int num) { this.m_deals_processed=num; } int NumProcessedDeals(void) const { return this.m_deals_processed; } //--- Add a deal to the deal array bool AddDeal(CDeal *deal); //--- Return the deal (1) by time in seconds, (2) by index in the list, //--- (3) opening deal by position ID, (4) current deal in the list CDeal *GetDealByTime(const datetime time); CDeal *GetDealByIndex(const int index); CDeal *GetDealInByPosID(const long pos_id); CDeal *GetDealCurrent(void); //--- Return (1) the number of deals in the list, (2) the index of the current deal in the list int DealsTotal(void) const { return this.m_list_deals.Total(); } int DealCurrentIndex(void) const { return this.m_index_next_deal; } //--- Return (1) symbol and (2) object description string Symbol(void) const { return this.m_symbol; } string Description(void) const { return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() ); } //--- Return the current (1) Bid and (2) Ask price, time in (3) seconds, (4) milliseconds double Bid(void); double Ask(void); datetime Time(void); long TimeMsc(void); //--- Open (1) long, (2) short position, (3) close a position by ticket ulong Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment); ulong Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment); bool ClosePos(const ulong ticket); //--- Return the result of comparing the current time with the specified one bool CheckTime(const datetime time) { return(this.Time()>=time); } //--- Sets the index of the next deal void SetNextDealIndex(void) { this.m_index_next_deal++; } //--- OnTester handler. Returns the number of deals processed by the tester. double OnTester(void) { ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals()); return this.m_deals_processed; } //--- Compares two objects to each other (comparison by symbol only) virtual int Compare(const CObject *node, const int mode=0) const { const CSymbolTrade *obj=node; return(this.Symbol()>obj.Symbol() ? 1 : this.Symbol()<obj.Symbol() ? -1 : 0); } //--- Constructors/destructor CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {} CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0) { this.m_trade.SetMarginMode(); this.m_trade.SetTypeFillingBySymbol(this.m_symbol); } ~CSymbolTrade(void) {} };
Schauen wir uns einige Methoden an.
- SetNumProcessedDeals() und NumProcessedDeals() setzen und geben die Anzahl der historischen Handelsgeschäfte zurück, die bereits vom Tester aus der Liste der Handelsgeschäfte aus der Datei bearbeitet wurden. Sie sind notwendig, um die Gültigkeit der Bearbeitung von historischen Handelsgeschäften zu überprüfen und um endgültige Statistiken über die Anzahl der vom Tester bearbeiteten Handelsgeschäfte zu erhalten;
- GetDealCurrent() gibt einen Zeiger auf das aktuelle historische Handelsgeschäft zurück, das vom Tester bearbeitet werden muss und als bearbeitet markiert ist;
- DealCurrentIndex() gibt den Index des historischen Handelsgeschäfts zurück, das derzeit für die Bearbeitung durch den Tester ausgewählt ist;
- SetNextDealIndex() setzt nach Abschluss der Bearbeitung des aktuellen historischen Handelsgeschäfts den Index des nächsten vom Tester zu bearbeitenden Handelsgeschäfts. Da alle historischen Handelsgeschäfte in der Liste nach der Zeit in Millisekunden sortiert sind, wird der Index des nächsten Handelsgeschäfts gesetzt, nachdem der Tester die Bearbeitung des vorherigen abgeschlossen hat. Auf diese Weise werden nacheinander alle Handelsgeschäfte in der Historie ausgewählt, die vom Tester zu dem Zeitpunkt bearbeitet werden, der in den Eigenschaften des aktuell ausgewählten Handelsgeschäfts festgelegt ist;
- CheckTime() prüft den Zeitpunkt des Auftretens der Zeit, die in den Eigenschaften des aktuellen historischen Handelsgeschäfts festgelegt wurde, im Tester. Die Logik ist wie folgt: Es gibt ein ausgewähltes Handelsgeschäft, das im Tester bearbeitet werden muss. Solange die Zeit im Tester kürzer ist als die im Handelsgeschäft aufgezeichnete Zeit, tun wir nichts – wir gehen einfach zum nächsten Tick über. Sobald die Zeit im Tester gleich oder größer als die Zeit im aktuell ausgewählten Handelsgeschäft wird (die Zeit im Tester darf nicht mit der Zeit im Handelsgeschäft übereinstimmen, daher wird die Zeit auch auf „größer“ geprüft), wird das Handelsgeschäft vom Tester abhängig von seinem Typ und der Art der Positionsänderung durch das Handelsgeschäft behandelt. Anschließend wird dieser Vorgang als erledigt markiert, der Index des nächsten Handelsgeschäfts wird gesetzt, und das von der Methode verwaltete Warten geht weiter, allerdings auf das nächste Handelsgeschäft:
- Die Funktion OnTester() wird von der Standardfunktion OnTester() des EA aufgerufen, zeigt den Symbolnamen im Journal sowie die Anzahl der historischen und vom Tester bearbeiteten Handelsgeschäfte an und gibt die Anzahl der bearbeiteten Handelsgeschäfte nach Handelsobjektsymbol zurück.
Die Klasse hat zwei Konstruktoren – Standard und parametrisch.
In den formalen Parametern des parametrischen Konstruktors wird der Name des Handelsobjekt-Symbols übergeben, der bei der Erstellung des Objekts verwendet werden soll, während das Handelsobjekt der Klasse CTrade den Modus für die Berechnung der Marge entsprechend den aktuellen Kontoeinstellungen sowie die Art der Auftragsausführung entsprechend den Einstellungen des Handelsobjekt-Symbols erhält:
//--- Constructors/destructor CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {} CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0) { this.m_trade.SetMarginMode(); this.m_trade.SetTypeFillingBySymbol(this.m_symbol); }
Die Methode, die ein Handelsgeschäft zum Array der Handelsgeschäfte hinzufügt:
//+------------------------------------------------------------------+ //| CSymbolTrade::Add a trade to the trades array | //+------------------------------------------------------------------+ bool CSymbolTrade::AddDeal(CDeal *deal) { //--- If the list already contains a deal with the deal ticket passed to the method, return 'true' this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET); if(this.m_list_deals.Search(deal)>WRONG_VALUE) return true; //--- Add a pointer to the deal to the list in sorting order by time in milliseconds this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); if(!this.m_list_deals.InsertSort(deal)) { ::PrintFormat("%s: Failed to add deal", __FUNCTION__); return false; } //--- All is successful return true; }
Der Zeiger auf das Deal-Objekt wird an die Methode übergeben. Wenn ein Handelsgeschäft mit einem solchen Ticket bereits in der Liste enthalten ist, wird true zurückgegeben. Andernfalls wird die Liste nach der Handelsgeschäftszeit in Millisekunden sortiert, und das Handelsgeschäft wird der Liste in der Sortierreihenfolge nach Zeit in ms hinzugefügt.
Die Methode, die einen Zeiger auf ein Deal-Objekt nach Zeit in Sekunden zurückgibt:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the deal object by time in seconds | //+------------------------------------------------------------------+ CDeal* CSymbolTrade::GetDealByTime(const datetime time) { DealTmp.SetTime(time); this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); int index=this.m_list_deals.Search(&DealTmp); return this.m_list_deals.At(index); }
Die Methode erhält die erforderliche Zeit. Wir setzen die an die Methode übergebene Zeit auf das temporäre Deal-Objekt, die Liste wird nach der Zeit in Millisekunden sortiert, und es wird nach dem Index des Deals gesucht, dessen Zeit gleich der an die Methode übergebenen Zeit ist (auf das temporäre Objekt gesetzt). Anschließend wird ein Zeiger auf das Handelsgeschäft in der Liste mit dem gefundenen Index zurückgegeben. Gibt es in der Liste kein Handelsgeschäft mit einer solchen Zeit, so ist der Index gleich -1, während aus der Liste NULL zurückgegeben wird.
Interessant ist, dass das Handelsgeschäft nach der Zeit in Sekunden durchsucht wird, wir aber die Liste nach der Zeit in Millisekunden sortieren. Tests haben gezeigt, dass, wenn die Liste auch nach Sekunden sortiert ist, einige Angebote nicht darin enthalten sind, obwohl sie definitiv existieren. Dies ist höchstwahrscheinlich auf die Tatsache zurückzuführen, dass es in einer Sekunde mehrere Handelsgeschäfte gibt, wobei die Zeit in Millisekunden angegeben wird. Außerdem wird der Zeiger auf ein zuvor abgewickeltes Handelsgeschäft zurückgegeben, da mehrere Transaktionen die gleiche Zeit in Sekunden haben.
Die Methode, die den Zeiger auf die offenen Position nach Positions-ID zurück:
//+------------------------------------------------------------------+ //|CSymbolTrade::Return the opening trade by position ID | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealInByPosID(const long pos_id) { int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL || deal.PositionID()!=pos_id) continue; if(deal.Entry()==DEAL_ENTRY_IN) return deal; } return NULL; }
Die Methode erhält die ID der Position, deren Eröffnungs-Deal gefunden werden muss. Als Nächstes wird in einer Schleife durch die Liste der Handelsgeschäfte das Handelsgeschäft ermittelt, dessen Positions-ID gleich der an die Methode übergebenen ist, und ein Zeiger auf das Handelsgeschäft zurückgegeben, dessen Positionsänderungsmethode gleich „Markteintritt“ (DEAL_ENTRY_IN) ist.
Die Methode, die den Zeiger auf das Deal-Objekt nach Index in der Liste zurückgibt:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the deal object by index in the list | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealByIndex(const int index) { return this.m_list_deals.At(index); }
Wir geben einfach den Zeiger auf das Objekt in der Liste mit dem an die Methode übergebenen Index zurück. Wenn der Index falsch ist, wird NULL zurückgegeben.
Die Methode, die den Zeiger auf das Handelsgeschäft zurück, auf das der aktuelle Handelsgeschäftsindex zeigt:
//+------------------------------------------------------------------+ //| Return the deal pointed to by the current deal index | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealCurrent(void) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); return this.GetDealByIndex(this.m_index_next_deal); }
Die Liste der Handelsgeschäfte wird nach Zeit in Millisekunden sortiert, und der Zeiger auf das Handelsgeschäft, dessen Index in die Klassenvariable m_index_next_deal geschrieben wird, wird zurückgegeben.
Die Methode, die den aktuellen Geldkurs (Bid) zurückgibt:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current Bid price | //+------------------------------------------------------------------+ double CSymbolTrade::Bid(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.bid; }
Wir holen uns die Daten des letzten Ticks in die m_tick Preisstruktur und geben den Geldkurs daraus zurück.
Die Methode liefert den aktuellen Briefkurs (Ask):
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current Ask price | //+------------------------------------------------------------------+ double CSymbolTrade::Ask(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.ask; }
Wir holen uns die Daten des letzten Ticks in die m_tick Preisstruktur und geben daraus den Ask Preis zurück.
Die Methode gibt die aktuelle Zeit in Sekunden zurück:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current time in seconds | //+------------------------------------------------------------------+ datetime CSymbolTrade::Time(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.time; }
Wir holen uns die Daten des letzten Ticks in die m_tick Preisstruktur und geben die Zeit daraus zurück.
Die Methode gibt die aktuelle Zeit in Millisekunden zurück:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the current time in milliseconds | //+------------------------------------------------------------------+ long CSymbolTrade::TimeMsc(void) { ::ResetLastError(); if(!::SymbolInfoTick(this.m_symbol, this.m_tick)) { ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError()); return 0; } return this.m_tick.time_msc; }
Wir holen die Daten des letzten Ticks in die m_tick-Preisstruktur und geben die Zeit in Millisekunden zurück.
Die Methode zur Eröffnung einer Kaufposition:
//+------------------------------------------------------------------+ //| CSymbolTrade::Open a long position | //+------------------------------------------------------------------+ ulong CSymbolTrade::Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment) { this.m_trade.SetExpertMagicNumber(magic); if(!this.m_trade.Buy(volume, this.m_symbol, 0, sl, tp, comment)) { return 0; } return this.m_trade.ResultOrder(); }
Die Methode empfängt die Parameter der eröffneten Kaufposition, setzt die erforderliche magische Positionsnummer des Handelsobjekts und sendet den Auftrag zur Eröffnung einer Kaufposition mit den angegebenen Parametern. Wenn die Eröffnung einer Position fehlschlägt, wird Null zurückgegeben, während bei Erfolg das Auftragsticket zurückgegeben wird, auf dem die eröffnete Position basiert.
Die Methode zur Eröffnung einer Verkaufsposition:
//+------------------------------------------------------------------+ //| CSymbolTrade::Open a short position | //+------------------------------------------------------------------+ ulong CSymbolTrade::Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment) { this.m_trade.SetExpertMagicNumber(magic); if(!this.m_trade.Sell(volume, this.m_symbol, 0, sl, tp, comment)) { return 0; } return this.m_trade.ResultOrder(); }
Ähnlich wie bei der vorherigen Methode, jedoch wird eine Verkaufsposition eröffnet.
Die Methode zur Schließung einer Position per Ticket:
//+------------------------------------------------------------------+ //| CSymbolTrade::Close position by ticket | //+------------------------------------------------------------------+ bool CSymbolTrade::ClosePos(const ulong ticket) { return this.m_trade.PositionClose(ticket); }
Gibt das Ergebnis des Aufrufs der Methode PositionClose() des Handelsobjekts der Klasse CTrade zurück.
Die Klasse der Handelssymbole ist fertig. Nun wollen wir sie in den EA implementieren, um die in der Datei gespeicherten historischen Handelsgeschäfte zu behandeln.
Analyse der Handelshistorie aus der Datei im Tester
Gehen wir nun zur EA-Datei \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 und fügen ein temporäres Objekt der neu erstellten Handelssymbolklasse hinzu – es wird benötigt, um das gewünschte Objekt in der Liste zu finden , in der die Zeiger auf solche Objekte gespeichert sind:
//+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ //--- global variables CSymbolTrade SymbTradeTmp; SDeal ExtArrayDeals[]={}; CArrayObj ExtListSymbols;
Wir haben ein Array historischer Handelsgeschäfte, auf dessen Grundlage wir eine Liste von Handelsobjekten erstellen können, in der sich Listen von Handelsgeschäften befinden, die zu dem Objektsymbol gehören. Das Deal-Array speichert Strukturen, die Deals beschreiben. Da das Handelsobjekt Listen von Handelsgeschäftsobjekten enthalten wird, müssen wir eine Funktion erstellen, die ein neues Handelsgeschäftsobjekt anlegt und die Handelsgeschäftseigenschaften aus den Feldern der Struktur, die das Handelsgeschäft beschreibt, auffüllt:
//+------------------------------------------------------------------+ //| Create a deal object from the structure | //+------------------------------------------------------------------+ CDeal *CreateDeal(SDeal &deal_str) { //--- If failed to create an object, inform of the error in the journal and return NULL CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol()); if(deal==NULL) { PrintFormat("%s: Error. Failed to create deal object"); return NULL; } //--- fill in the deal properties from the structure fields deal.SetOrder(deal_str.order); // Order the deal was based on deal.SetPositionID(deal_str.pos_id); // Position ID deal.SetTimeMsc(deal_str.time_msc); // Time in milliseconds deal.SetTime(deal_str.time); // Time deal.SetVolume(deal_str.volume); // Volume deal.SetPrice(deal_str.price); // Price deal.SetProfit(deal_str.profit); // Profit deal.SetCommission(deal_str.commission); // Deal commission deal.SetSwap(deal_str.swap); // Accumulated swap when closing deal.SetFee(deal_str.fee); // Fee for making a deal charged immediately after performing a deal deal.SetSL(deal_str.sl); // Stop Loss level deal.SetTP(deal_str.tp); // Take Profit level deal.SetType(deal_str.type); // Type deal.SetEntry(deal_str.entry); // Position change method deal.SetReason(deal_str.reason); // Deal execution reason or source deal.SetMagic(deal_str.magic); // EA ID deal.SetComment(deal_str.Comment()); // Deal comment deal.SetExternalID(deal_str.ExternalID()); // Deal ID in an external trading system (on the exchange) //--- Return the pointer to a created object return deal; }
Die Funktion empfängt die Handelsgeschäftsstruktur, ein neues Handelsgeschäftsobjekt wird erstellt und seine Eigenschaften werden mit Werten aus den Strukturfeldern gefüllt.
Die Funktion gibt einen Zeiger auf das neu erstellte Objekt zurück. Wenn bei der Erstellung des Objekts ein Fehler auftritt, wird NULL zurückgegeben.
Wir wollen eine Funktion schreiben, die eine Liste von Handelssymbolen erstellt:
//+------------------------------------------------------------------+ //| Create an array of used symbols | //+------------------------------------------------------------------+ bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols) { bool res=true; // result int total=(int)array_deals.Size(); // total number of deals in the array //--- if the deal array is empty, return 'false' if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- in a loop through the deal array CSymbolTrade *SymbolTrade=NULL; for(int i=0; i<total; i++) { //--- get the next deal and, if it is neither buy nor sell, move on to the next one SDeal deal_str=array_deals[i]; if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL) continue; //--- find a trading object in the list whose symbol is equal to the deal symbol string symbol=deal_str.Symbol(); SymbTradeTmp.SetSymbol(symbol); list_symbols.Sort(); int index=list_symbols.Search(&SymbTradeTmp); //--- if the index of the desired object in the list is -1, there is no such object in the list if(index==WRONG_VALUE) { //--- we create a new trading symbol object and, if creation fails, //--- add 'false' to the result and move on to the next deal SymbolTrade=new CSymbolTrade(symbol); if(SymbolTrade==NULL) { res &=false; continue; } //--- if failed to add a symbol trading object to the list, //--- delete the newly created object, add 'false' to the result //--- and we move on to the next deal if(!list_symbols.Add(SymbolTrade)) { delete SymbolTrade; res &=false; continue; } } //--- otherwise, if the trading object already exists in the list, we get it by index else { SymbolTrade=list_symbols.At(index); if(SymbolTrade==NULL) continue; } //--- if the current deal is not yet in the list of deals of the symbol trading object if(SymbolTrade.GetDealByTime(deal_str.time)==NULL) { //--- create a deal object according to its sample structure CDeal *deal=CreateDeal(deal_str); if(deal==NULL) { res &=false; continue; } //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value res &=SymbolTrade.AddDeal(deal); } } //--- return the final result of creating trading objects and adding deals to their lists return res; }
Die Logik der Funktion ist in den Kommentaren ausführlich beschrieben. Wir analysieren jedes aufeinanderfolgende Handelsgeschäft in einer Schleife durch die Liste der historischen Handelsgeschäfte. Wir prüfen das Symbol, und wenn es noch kein Handelsobjekt für dieses Symbol gibt, erstellen wir ein neues Handelsobjekt und speichern es in der Liste. Wenn es bereits existiert, holen wir uns einfach den Zeiger auf das Symbolhandelsobjekt eines Handelsgeschäfts aus der Liste. Anschließend prüfen wir auf die gleiche Weise, ob ein solches Handelsgeschäft in der Liste der Handelsgeschäfte des Handelsobjekts vorhanden ist, und fügen es der Liste hinzu, wenn es dort nicht vorhanden ist. Als Ergebnis des Durchlaufs durch alle historischen Handelsgeschäfte erhalten wir eine Liste von Handelsobjekten nach Symbolen, die Listen von Handelsgeschäften enthalten, die zu dem Objektsymbol gehören.
Die Liste der Handelsobjekte kann mit der folgenden Funktion an das Journal gesendet werden:
//+------------------------------------------------------------------+ //| Display a list of symbol trading objects in the journal | //+------------------------------------------------------------------+ void SymbolsArrayPrint(CArrayObj *list_symbols) { int total=list_symbols.Total(); if(total==0) return; Print("Symbols used in trading:"); for(int i=0; i<total; i++) { string index=StringFormat("% 3d", i+1); CSymbolTrade *obj=list_symbols.At(i); if(obj==NULL) continue; PrintFormat("%s. %s",index, obj.Description()); } }
In einer Schleife durch die Liste der Handelssymbole wird das nächste Objekt ermittelt und seine Beschreibung im Journal angezeigt. Im Journal sieht das etwa so aus:
Symbols used in trading: 1. AUDUSD trade object. Total deals: 218 2. EURJPY trade object. Total deals: 116 3. EURUSD trade object. Total deals: 524 4. GBPUSD trade object. Total deals: 352 5. NZDUSD trade object. Total deals: 178 6. USDCAD trade object. Total deals: 22 7. USDCHF trade object. Total deals: 250 8. USDJPY trade object. Total deals: 142 9. XAUUSD trade object. Total deals: 118
Wir haben jetzt ein Objekt der Klasse Deal. Fügen wir noch eine Funktion hinzu, die eine Handelsgeschäftsbeschreibung zurückgibt:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string DealDescription(CDeal *deal, const int index) { string indexs=StringFormat("% 5d", index); if(deal.TypeDeal()!=DEAL_TYPE_BALANCE) return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f", indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()), deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(), TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP())); else return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s", indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()), deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time()))); }
Diese Funktion wiederholt vollständig die Logik der gleichen Funktion, die eine Beschreibung der Handelsgeschäftsstruktur zurückgibt. Hier wird jedoch der Zeiger auf das Handelsgeschäftsobjekt an die Funktion übergeben und nicht die Handelsgeschäftsstruktur.
Lassen Sie uns nun die Ereignisbehandlung von OnInit() bis zur logischen Schlussfolgerung erstellen.
Wir fügen die Handhabung des EA-Starts im Tester hinzu, erstellen die Liste der Handelsobjekte und greifen auf jedes Symbol zu, das im Handel verwendet wurde, um seine Historie zu laden und Chart-Fenster für diese Symbole im Tester zu öffnen:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- If the EA is not running in the tester if(!MQLInfoInteger(MQL_TESTER)) { //--- prepare a file with all historical deals if(!PreparesDealsHistoryFile(ExtArrayDeals)) return(INIT_FAILED); //--- print all deals in the journal after loading them from the file if(InpShowDataInLog) DealsArrayPrint(ExtArrayDeals); //--- get the first balance deal, create the message text and display it using Alert SDeal deal=ExtArrayDeals[0]; long leverage=AccountInfoInteger(ACCOUNT_LEVERAGE); double start_money=deal.profit; datetime first_time=deal.time; string start_time=TimeToString(deal.time, TIME_DATE); string message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage); //--- notify via alert of the recommended parameters of the strategy tester for starting the test Alert(message); } //--- The EA has been launched in the tester else { //--- read data from the file into the array ulong file_size=0; ArrayResize(ExtArrayDeals, 0); if(!FileReadDealsToArray(ExtArrayDeals, file_size)) { PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError()); return(INIT_FAILED); } //--- report the number of bytes read from the file and writing the deals array in the journal. PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size()); } //--- Create a list of trading objects by symbols from the array of historical deals if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols)) { Print("Errors found while creating symbol list"); return(INIT_FAILED); } //--- Print the created list of deals in the journal SymbolsArrayPrint(&ExtListSymbols); //--- Access each symbol to start downloading historical data //--- and opening charts of traded symbols in the strategy tester datetime array[]; int total=ExtListSymbols.Total(); for(int i=0; i<total; i++) { CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; CopyTime(obj.Symbol(), PERIOD_CURRENT, 0, 1, array); } //--- All is successful return(INIT_SUCCEEDED); }
In OnDeinit() des EA löschen wir die erstellten Arrays und Listen mit Hilfe des EA:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- clear the created lists and arrays ExtListSymbols.Clear(); ArrayFree(ExtArrayDeals); }
In OnTick() des EA verarbeiten wir die Liste der Handelsgeschäfte aus der Datei im Tester:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- work only in the strategy tester if(!MQLInfoInteger(MQL_TESTER)) return; //--- Handle the list of deals from the file in the tester TradeByHistory(InpTestedSymbol, InpTestedMagic); }
Betrachten wir diese Funktion im Detail. Im Allgemeinen wurde die Logik für den Umgang mit historischen Handelsgeschäften ursprünglich wie folgt dargestellt:
- die Tick-Zeit abrufen,
- einen Deal mit einer solchen Zeit abrufen,
- ein Handelsgeschäft im Tester abwickeln.
Auf den ersten Blick ist die einfache und logische Struktur bei der Umsetzung völlig gescheitert. Das Problem ist, dass im Tester die Tick-Zeit nicht immer mit der Handelsgeschäftszeit übereinstimmt. Sogar in Millisekunden. Beim Testen aller Ticks auf der Grundlage echter Ticks, die vom selben Server stammen, gingen daher Abschlüsse verloren. Wir könnten die Tick-Zeit kennen, wir könnten sicher wissen, dass es genau zu diesem Zeitpunkt ein Handelsgeschäft gibt, aber der Tester sieht es nicht – es gibt keinen Tick mit der gleichen Zeit wie das Handelsgeschäft. Aber es gibt einen Tick mit der Zeit vor und mit der Zeit nach der Handelsgeschäftszeit. Dementsprechend kann die Logik nicht um Ticks und deren Zeitpunkt, sondern um Handelsgeschäfte aufgebaut werden:
- Die Handelsgeschäfte sind in der Liste nach der Zeit ihres Erscheinens in Millisekunden sortiert. Setzt den Index des allerersten Handelsgeschäfts auf den Index des aktuellen Handelsgeschäfts.
- Ein Handelsgeschäft über den aktuellen Handelsgeschäftsindex auswählen und seine Zeit abfragen.
- Mit dieser Zeit auf einen Tick warten:
- Wenn die Tick-Zeit kleiner ist als die Deal-Zeit, wird auf den nächsten Tick gewartet.
- Wenn die Tickzeit gleich oder größer ist als die Handelsgeschäftszeit, wird das Handelsgeschäft abgewickelt, die Tatsache registriert, dass es bereits abgewickelt wurde, und der Index des nächsten Handelsgeschäfts als Index des aktuellen gesetzt.
- Ab Punkt 2 wiederholen, bis der Test beendet ist.
Diese Struktur ermöglicht es uns, auf den Zeitpunkt jedes nachfolgenden Handelsgeschäfts zu warten und es im Tester auszuführen. In diesem Fall achten wir nicht auf den Preis des Handelsgeschäfts, sondern kopieren es einfach, wenn es soweit ist. Selbst wenn die Tickzeit im Tester etwas später ist als in der Realität, ist das in Ordnung. Die Hauptsache ist, den Handel zu kopieren. Die Tatsache, dass der Vorgang bereits vom Tester bearbeitet wurde, wird durch einen Wert ungleich Null der Eigenschaft „position ticket in tester“ angezeigt. Ist dieser Wert gleich Null, so bedeutet dies, dass dieser Vorgang im Tester noch nicht bearbeitet wurde. Nachdem dieses Handelsgeschäft im Tester ausgeführt wurde, wird das Ticket der Position, zu der dieses Handelsgeschäft im Tester gehört, in diese Eigenschaft eingetragen.
Fügen wir nun die Funktion hinzu, die die oben beschriebene Logik implementiert:
//+------------------------------------------------------------------+ //| Trading by history | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // number of trading objects in the list //--- in a loop by all symbol trading objects for(int i=0; i<total; i++) { //--- get another trading object CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- get the current deal pointed to by the deal list index CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- sort the deal by magic number and symbol if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- sort the deal by type (only buy/sell deals) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- if this is a deal already handled in the tester, move on to the next one if(deal.TicketTester()>0) continue; //--- if the deal time has not yet arrived, move to the next trading object of the next symbol if(!obj.CheckTime(deal.Time())) continue; //--- in case of a market entry deal ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- open a position by deal type double sl=0; double tp=0; ulong ticket=(type==DEAL_TYPE_BUY ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0); //--- if a position is opened (we received its ticket) if(ticket>0) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- get the position ID in the tester and write it to the properties of the deal object long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- in case of a market exit deal if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- get a deal a newly opened position is based on CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- get the position ticket in the tester from the properties of the opening deal //--- if the ticket is zero, then most likely the position in the tester is already closed ulong ticket_tester=deal_in.TicketTester(); if(ticket_tester==0) { PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester()); obj.SetNextDealIndex(); continue; } //--- if the position is closed by ticket if(obj.ClosePos(ticket_tester)) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- if a ticket is now set in the deal object, then the deal has been successfully handled - //--- set the deal index in the list to the next deal if(deal.TicketTester()>0) { obj.SetNextDealIndex(); } } }
Dieser Code kopiert vollständig den ursprünglichen Handel auf dem Konto, dessen Handelsgeschäfte in der Datei registriert sind. Alle Positionen werden ohne Stop-Order eröffnet. Mit anderen Worten, die Werte für StopLoss und TakeProfit werden nicht von echten Handelsgeschäften in die Positionseröffnungsmethoden kopiert. Dies vereinfacht die Nachverfolgung von Handelsgeschäften, da auch geschlossene Handelsgeschäfte aufgelistet werden und der Tester sie unabhängig davon behandelt, wie die Position geschlossen wurde – durch StopLoss oder TakeProfit.
Kompilieren Sie den EA und starten Sie ihn auf dem Chart. Als Ergebnis wird die Datei HistoryDealsData.bin im freigegebenen Ordner der Client-Terminals mit dem Pfad „C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files“ im Unterordner „TradingByHistoryDeals“ erstellt, und im Chart wird eine Warnung mit einer Meldung über die gewünschten Testereinstellungen angezeigt:

Führen wir nun den EA im Tester aus, indem wir den angegebenen Datumsbereich, die Ersteinlage und den Hebel in den Testereinstellungen auswählen:

Führen Sie den Test für alle gehandelten Symbole und den magischen Zahlen durch:

Es stellte sich heraus, dass der gesamte Handel uns einen Verlust von 550 USD einbrachte. Ich frage mich, was passieren würde, wenn wir unterschiedliche Stop-Orders setzen würden?
Schauen wir uns das mal an.
Anpassung von Stop-Aufträgen
Speichern Sie den EA in demselben Ordner \MQL5\Experts\TradingByHistoryDeals\ als TradingByHistoryDeals_SLTP.mq5.
Fügen Sie eine Enumeration von Testmethoden hinzu und unterteilen Sie die Eingaben in Gruppen, indem Sie eine Gruppe für die Einstellung von Stop-Order-Parametern sowie zwei neue Variablen auf globaler Ebene hinzufügen, über die StopLoss- und TakeProfit-Werte an Handelsobjekte übergeben werden können:
//+------------------------------------------------------------------+ //| TradingByHistoryDeals_SLTP.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "SymbolTrade.mqh" enum ENUM_TESTING_MODE { TESTING_MODE_ORIGIN, /* Original trading */ TESTING_MODE_SLTP, /* Specified StopLoss and TakeProfit values */ }; //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input group "Strategy parameters" input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ input group "Stops parameters" input ENUM_TESTING_MODE InpTestingMode = TESTING_MODE_ORIGIN; /* Testing Mode */ input int InpStopLoss = 300; /* StopLoss in points */ input int InpTakeProfit = 500; /* TakeProfit in points */ //--- global variables CSymbolTrade SymbTradeTmp; SDeal ExtArrayDeals[]={}; CArrayObj ExtListSymbols; int ExtStopLoss; int ExtTakeProfit;
Im OnInit()-Handler werden die Werte der durch die Eingaben gesetzten Stop-Orders angepasst und in Variablen geschrieben:
int OnInit() { //--- Adjust the stops ExtStopLoss =(InpStopLoss<1 ? 0 : InpStopLoss); ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit); //--- If the EA is not running in the tester
Fügen wir die Funktionen hinzu, die die korrekten Werte für die StopLoss- und TakeProfit-Kurse in Bezug auf das für das Symbol festgelegte StopLevel berechnen:
//+------------------------------------------------------------------+ //| Return correct StopLoss relative to StopLevel | //+------------------------------------------------------------------+ double CorrectStopLoss(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int stop_loss, const int spread_multiplier=2) { if(stop_loss==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL)) return 0; int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS); double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT); double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK)); return (order_type==ORDER_TYPE_BUY ? NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) : NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg) ); } //+------------------------------------------------------------------+ //| Return correct TakeProfit relative to StopLevel | //+------------------------------------------------------------------+ double CorrectTakeProfit(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int take_profit, const int spread_multiplier=2) { if(take_profit==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL)) return 0; int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS); double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT); double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK)); return (order_type==ORDER_TYPE_BUY ? NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) : NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg) ); } //+------------------------------------------------------------------+ //| Return StopLevel in points | //+------------------------------------------------------------------+ int StopLevel(const string symbol_name, const int spread_multiplier) { int spread=(int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD); int stop_level=(int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL); return(stop_level==0 ? spread*spread_multiplier : stop_level); }
Um StopLoss- und TakeProfit-Levels zu setzen, darf der Stop-Order-Kurs nicht näher als der aktuelle Kurs sein, um den StopLevel-Abstand. Wenn der StopLevel für ein Symbol den Wert Null hat, dann entspricht die verwendete StopLevel-Größe zwei oder manchmal drei für das Symbol festgelegten Spreads. Diese Funktionen verwenden einen Multiplikator zur Verdopplung des Spread. Dieser Wert wird in formalen Parametern von Funktionen übergeben und hat einen Standardwert von 2. Wenn es notwendig ist, den Wert des Multiplikators zu ändern, müssen wir den Funktionen beim Aufruf einen anderen erforderlichen Wert übergeben. Die Funktionen geben die korrekten Preise für StopLoss und TakeProfit zurück.
Fügen Sie in die Handelsfunktion TradeByHistory() neue Codeblöcke ein, die den Handelsmodus im Tester berücksichtigen und StopLoss- und TakeProfit-Werte festlegen, wenn der Test mit den angegebenen Stop-Order-Werten ausgewählt wurde. Im Positionsschließungsblock müssen wir Positionen nur dann schließen, wenn die Testart „ursprünglicher Handel“ ist. Wenn das Testen mit spezifizierten Stop-Order-Werten ausgewählt ist, sollte das Schließen von Handelsgeschäften ignoriert werden – der Tester schließt Positionen automatisch auf der Grundlage der spezifizierten StopLoss- und TakeProfit-Werte. Das Einzige, was wir beim Handel mit Stop-Orders tun müssen, wenn Abschlussgeschäfte abgewickelt werden, ist, sie als abgewickelt zu markieren und zum nächsten Handelsgeschäft überzugehen.
//+------------------------------------------------------------------+ //| Trading by history | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // number of trading objects in the list //--- in a loop by all symbol trading objects for(int i=0; i<total; i++) { //--- get another trading object CSymbolTrade *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- get the current deal pointed to by the deal list index CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- sort the deal by magic number and symbol if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- sort the deal by type (only buy/sell deals) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- if this is a deal already handled in the tester, move on to the next one if(deal.TicketTester()>0) continue; //--- if the deal time has not yet arrived, move to the next trading object of the next symbol if(!obj.CheckTime(deal.Time())) continue; //--- in case of a market entry deal ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- set the sizes of stop orders depending on the stop setting method double sl=0; double tp=0; if(InpTestingMode==TESTING_MODE_SLTP) { ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss); tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit); } //--- open a position by deal type ulong ticket=(type==DEAL_TYPE_BUY ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0); //--- if a position is opened (we received its ticket) if(ticket>0) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- get the position ID in the tester and write it to the properties of the deal object long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- in case of a market exit deal if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- get a deal a newly opened position is based on CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- get the position ticket in the tester from the properties of the opening deal //--- if the ticket is zero, then most likely the position in the tester is already closed ulong ticket_tester=deal_in.TicketTester(); if(ticket_tester==0) { PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester()); obj.SetNextDealIndex(); continue; } //--- if we reproduce the original trading history in the tester, if(InpTestingMode==TESTING_MODE_ORIGIN) { //--- if the position is closed by ticket if(obj.ClosePos(ticket_tester)) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped //--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object else { obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- if a ticket is now set in the deal object, then the deal has been successfully handled - //--- set the deal index in the list to the next deal if(deal.TicketTester()>0) { obj.SetNextDealIndex(); } } }
Im OnTester() des EA berechnen wir die Gesamtzahl der im Tester abgewickelten Handelsgeschäfte und geben Sie diese zurück:
//+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester(void) { //--- calculate and return the total number of deals handled in the tester double ret=0.0; int total=ExtListSymbols.Total(); for(int i=0; i<total; i++) { CSymbolTrade *obj=ExtListSymbols.At(i); if(obj!=NULL) ret+=obj.OnTester(); } return(ret); }
Darüber hinaus hat jedes Symbolhandelsobjekt seine eigene OnTester()-Funktion, die hier aufgerufen wird und seine Daten im Journal ausgibt. Am Ende des Tests erhalten wir die folgenden Meldungen im Testerprotokoll:
2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol AUDUSD: Total deals: 218, number of processed deals: 216 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURJPY: Total deals: 116, number of processed deals: 114 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURUSD: Total deals: 524, number of processed deals: 518 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol GBPUSD: Total deals: 352, number of processed deals: 350 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol NZDUSD: Total deals: 178, number of processed deals: 176 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCAD: Total deals: 22, number of processed deals: 22 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCHF: Total deals: 250, number of processed deals: 246 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDJPY: Total deals: 142, number of processed deals: 142 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol XAUUSD: Total deals: 118, number of processed deals: 118 2025.01.22 23:49:15.951 Core 1 final balance 3591.70 pips 2025.01.22 23:49:15.951 Core 1 OnTester result 1902
Kompilieren Sie den EA und führen Sie ihn mit denselben Testeinstellungen aus, legen Sie jedoch als Testtyp „Specified StopLoss and TakeProfit values“ fest und setzen Sie die Werte für StopLoss und TakeProfit auf 100 bzw. 500 Punkte:

Im letzten Test, als wir den ursprünglichen Handel testeten, hatten wir einen Verlust von 550 USD. Wenn wir nun den StopLoss für alle Positionen durch einen Wert von 100 Punkten und den TakeProfit durch 500 Punkte ersetzen, erhalten wir einen Gewinn von 590 Punkten. Dies wurde durch eine einfache Ersetzung von Stop-Orders erreicht, ohne dass die Besonderheiten der verschiedenen gehandelten Symbole berücksichtigt wurden. Wenn wir unsere eigenen Stop-Order-Größen für jedes der gehandelten Symbole wählen, kann das Testdiagramm höchstwahrscheinlich ausgeglichen werden.
Schlussfolgerung
In diesem Artikel haben wir ein kleines Experiment mit einer Handelshistorie im Stil von „Was wäre wenn...“ durchgeführt. Ich denke, dass solche Experimente durchaus zu interessanten Lösungen führen können, um den eigenen Handelsstil zu ändern. Im nächsten Artikel werden wir ein weiteres Experiment dieser Art durchführen, indem wir verschiedene Positions-Trailing-Stops einbeziehen. Die Dinge werden bald noch interessanter werden.
Alle hier besprochenen EAs und Klassen sind unten beigefügt. Sie können sie herunterladen und studieren sowie mit ihnen in Ihrem eigenen Handelskonto experimentieren. Sie können den Archivordner sofort in Ihr MQL5-Client-Terminalverzeichnis entpacken, und alle Dateien werden in den gewünschten Unterordnern abgelegt.
Die Programme dieses Artikels:
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | SymbolTrade.mqh | Klassenbibliothek | Handelsgeschäftsstruktur und Klassenbibliothek, Symbolhandelsklasse |
| 2 | TradingByHistoryDeals.mq5 | Expert Advisor | Der EA zum Anzeigen von Handelsgeschäften und Abschlüssen, die auf einem Konto im Tester durchgeführt wurden |
| 3 | TradingByHistoryDeals_SLTP.mq5 | Expert Advisor | Der EA zum Anzeigen und Ändern von Handelsgeschäften und Abschlüssen, die auf dem Konto im Tester unter Verwendung von StopLoss und TakeProfit durchgeführt werden |
| 4 | MQL5.zip | ZIP-Archiv | Das Archiv mit den oben genannten Dateien kann in das MQL5-Verzeichnis des Client-Terminals entpackt werden |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/16952
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Artificial Tribe Algorithm (ATA)
Analyse des Binärcodes der Börsenkurse (Teil I): Ein neuer Blick auf die technische Analyse
Neuronale Netze im Handel: Ein hybrider Handelsrahmen mit prädiktiver Kodierung (letzter Teil)
Neuronale Netze im Handel: Ein hybrider Handelsrahmen mit prädiktiver Kodierung (StockFormer)
- 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.
Wo liegt der Fehler?
Wenn die Datei vor dem Lesen kleiner als das Array ist, ändert sich die Größe des Arrays nicht.
Ein ähnlicher Fehler kann bei der Verwendung von ArrayCopy auftreten.Sie ignorieren eine gute Funktion
Was ist der Vorteil?
Was ist der Vorteil?
Prägnanz und Geschwindigkeit der Ausführung (ganz auf der MQ-Seite).
Zeigen Sie bitte die standardmäßigen selbstdruckenden und selbstausfüllenden Strukturen.
In der Kürze und Geschwindigkeit der Ausführung (ganz auf der MQ-Seite).