
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 15): Den EA für den realen Handel vorbereiten
Einführung
In den vorangegangenen Artikeln haben wir bereits einige Ergebnisse erzielt, aber es gibt noch viel zu tun. Das Endergebnis, das wir gerne sehen würden, ist ein Multi-Währungs-EA, der so eingestellt werden kann, dass er auf einem realen Konto oder mehreren realen Konten mit verschiedenen Brokern funktioniert. Bisher haben sich unsere Bemühungen darauf konzentriert, während der Testphase gute Handelsergebnisse zu erzielen, da es sonst unmöglich ist, mit einem entwickelten EA auf einem realen Konto gute Handelsergebnisse zu erzielen. Jetzt, da wir mehr oder weniger gute Testergebnisse haben, können wir uns ein wenig mit der Sicherstellung des korrekten Betriebs auf einem echten Konto beschäftigen.
Wir haben diesen Aspekt der Entwicklung des EA bereits teilweise angesprochen. Insbesondere die Entwicklung eines Risikomanagers war ein Schritt zur Sicherstellung der Einhaltung von Anforderungen, die während des eigentlichen Handelsprozesses auftreten können. Ein Risikomanager wird nicht benötigt, um Handelsideen zu testen, da er ein wichtiges, aber zusätzliches Instrument ist.
Im Rahmen dieses Artikels werden wir versuchen, weitere wichtige Mechanismen vorzustellen, ohne die es nicht ratsam ist, mit dem Handel auf echten Konten zu beginnen. Da es sich dabei um Dinge handelt, die Situationen behandeln sollen, die nicht auftreten, wenn der EA im Strategietester ausgeführt wird, müssen wir höchstwahrscheinlich zusätzliche EAs entwickeln, um sie zu debuggen und die Gültigkeit ihrer Funktionsweise zu überprüfen.
Der Weg ist vorgezeichnet
Beim Handel mit realen Konten gibt es eine ganze Reihe von Details, die beachtet werden müssen. Konzentrieren wir uns zunächst auf einige von ihnen, die im Folgenden aufgeführt sind:
- Ersetzung von Symbolen. Wir haben Optimierungen vorgenommen und die Initialisierungsstrings der EAs mit sehr spezifischen Namen von Handelsinstrumenten (Symbolen) gestaltet. Es kann jedoch vorkommen, dass die Namen der Handelsinstrumente auf einem realen Konto von denen abweichen, die wir verwendet haben. Zu den möglichen Unterschieden gehören beispielsweise Suffixe oder Präfixe in den Namen (EURGBP.x oder xEURGBP anstelle von EURGBP) oder die Verwendung einer anderen Großschreibung (eurgbp anstelle von EURGBP). In Zukunft könnte die Liste der Handelssymbole um solche erweitert werden, deren Unterschiede in den Bezeichnungen noch bedeutender sein werden. Daher ist es notwendig, Regeln für die Ersetzung von Namen von Handelsinstrumenten festzulegen, damit der EA mit den Symbolen arbeiten kann, die ein bestimmter Broker verwendet.
- Handelsabschlussmodus. Da wir planen, die Zusammensetzung und die Einstellungen der Handelsstrategie-Instanzen, die gleichzeitig innerhalb des EA arbeiten, regelmäßig zu aktualisieren, ist es wünschenswert, die Möglichkeit zu bieten, einen bereits funktionierenden EA in einen speziellen Modus zu versetzen, in dem er „nur zum Schließen“ arbeitet, d.h. er wird sich bemühen, den Handel durch Schließen (vorzugsweise mit einem Gesamtgewinn) aller offenen Positionen zu beenden. Dies kann einige Zeit in Anspruch nehmen, wenn wir beschließen, den Handel mit diesem EA zu beenden, wenn wir bei offenen Positionen Verluste erleiden.
- Wiederherstellen nach einem Neustart. Damit ist die Fähigkeit des EA gemeint, seine Arbeit nach einem Neustart des Terminals fortzusetzen, der durch verschiedene Gründe verursacht werden kann. Es ist unmöglich, sich gegen einige dieser Ursachen zu wappnen. Der EA sollte jedoch nicht nur weiter funktionieren, sondern genau so, wie er funktioniert hätte, wenn es keinen Neustart gegeben hätte. Daher muss sichergestellt werden, dass der EA während des Betriebs alle notwendigen Informationen speichert, die es ihm ermöglichen, seinen Zustand nach einem Neustart wiederherzustellen.
Lassen Sie uns mit der Umsetzung unserer Pläne beginnen.
Ersetzung von Symbolen
Beginnen wir mit der einfachsten Sache — der Möglichkeit, Regeln für die Ersetzung von Namen von Handelssymbolen in den Einstellungen des EAs festzulegen. Die Unterschiede bestehen in der Regel in der Verwendung zusätzlicher Suffixe und/oder Präfixe. Daher können wir auf den ersten Blick zwei neue Parameter für das Setzen von Suffixen und Präfixen zu den Eingaben hinzufügen.
Diese Methode ist jedoch weniger flexibel, da sie nur einen festen Algorithmus zur Erlangung des korrekten Symbolnamens aus den ursprünglichen Namen der Initialisierungszeichenfolge zulässt. Außerdem wird für die Umwandlung in Kleinbuchstaben ein weiterer Parameter benötigt. Deshalb werden wir eine andere Methode einführen.
Wir fügen dem EA einen Parameter hinzu, der eine Zeichenkette wie diese enthält:
<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>
Dabei steht <Symbol[i]> für die ursprünglichen Namen der Handelssymbole, die im Initialisierungsstring verwendet werden, während <TargetSymbol[i]> für die Zielnamen der Handelssymbole steht, die für den tatsächlichen Handel verwendet werden. Zum Beispiel:
Der Wert dieses Parameters wird an eine spezielle Methode des EA-Objekts (die Klasse CVirtualAdvisor) übergeben, die alle weiteren erforderlichen Aktionen durchführt. Wird dieser Methode eine leere Zeichenfolge übergeben, sind keine Änderungen an den Namen der Handelssymbole erforderlich.
Nennen wir diese Methode SymbolsReplace und fügen wir ihren Aufruf dem Code der EA-Initialisierungsfunktion hinzu:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input string symbolsReplace_ = ""; // - Symbol replacement rules datetime fromDate = TimeCurrent(); CVirtualAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Create an EA handling virtual positions expert = NEW(expertParams); // If the EA is not created, then return an error if(!expert) return INIT_FAILED; // If an error occurred while replacing symbols, then return an error if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED; // Successful initialization return(INIT_SUCCEEDED); }
Wir speichern die an der Datei SimpleVolumesExpert.mq5 vorgenommenen Änderungen im aktuellen Ordner.
Fügen wir eine Beschreibung der EA-Methode hinzu, die die Ersetzung von Symbolnamen in der EA-Klasse und ihrer Implementierung vornimmt. In dieser Methode wird die übergebene Ersetzungszeichenfolge in ihre Bestandteile zerlegt, wobei sie zunächst durch ein Semikolon „;“ und dann durch ein Gleichheitszeichen „=“ getrennt wird. Aus den erhaltenen Teilen wird ein Glossar erstellt, das die Namen eines Quellensymbols mit dem Namen eines Zielsymbols verbindet. Das Glossar wird dann der Reihe nach an jede Instanz der Handelsstrategie weitergegeben, damit diese die notwendige Ersetzung vornehmen kann, wenn ihre Symbole als Schlüssel in diesem Glossar vorhanden sind.
Bei jedem Schritt, bei dem ein Fehler auftreten könnte, aktualisieren wir die Ergebnisvariable, damit die vorgelagerte Funktion weiß, ob die Ersetzung der Symbolnamen fehlgeschlagen ist. In einem solchen Fall meldet der EA einen Initialisierungsfehler.
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... public: ... bool SymbolsReplace(const string p_symbolsReplace); // Replace symbol names }; ... //+------------------------------------------------------------------+ //| Replace symbol names | //+------------------------------------------------------------------+ bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) { // If the replacement string is empty, then do nothing if(p_symbolsReplace == "") { return true; } // Variable for the result bool res = true; string symbolKeyValuePairs[]; // Array for individual replacements string symbolPair[]; // Array for two names in one replacement // Split the replacement string into parts representing one separate replacement StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs); // Glossary for mapping target symbol to source symbol CHashMap<string, string> symbolsMap; // For all individual replacements FOREACH(symbolKeyValuePairs, { // Get the source and target symbols as two array elements StringSplit(symbolKeyValuePairs[i], '=', symbolPair); // Check if the target symbol is in the list of available non-custom symbols bool custom = false; res &= SymbolExist(symbolPair[1], custom); // If the target symbol is not found, then report an error and exit if(!res) { PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]); return res; } // Add a new element to the glossary: key is the source symbol, while value is the target symbol res &= symbolsMap.Add(symbolPair[0], symbolPair[1]); }); // If no errors occurred, then for all strategies we call the corresponding replacement method if(res) { FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap)); } return res; } //+------------------------------------------------------------------+
Wir speichern die Änderungen in der Datei VirtualAdvisor.mqh Datei im aktuellen Ordner.
Wir fügen der Handelsstrategieklasse eine gleichnamige Methode hinzu, die jedoch nicht mehr eine Zeichenkette mit Substitutionen als Argument akzeptiert, sondern stattdessen das Glossar der Substitutionen. Leider können wir seine Implementierung nicht in die Klasse CVirtualStrategy aufnehmen, da wir noch nichts über die verwendeten Handelssymbole auf Klassenebene wissen. Machen wir sie also virtuell und verlagern wir die Verantwortung für die Implementierung auf eine niedrigere Ebene — in die untergeordneten Klassen.
//+------------------------------------------------------------------+ //| Class of a trading strategy with virtual positions | //+------------------------------------------------------------------+ class CVirtualStrategy : public CStrategy { ... public: ... // Replace symbol names virtual bool SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { return true; } };
Wir speichern die Änderungen in der Datei VirtualStrategy.mqh Datei im aktuellen Ordner.
Bisher gibt es nur eine untergeordnete Klasse, die die Eigenschaft m_symbol besitzt, in der der Name des Handelssymbols gespeichert wird. Fügen wir die Methode SymbolsReplace() hinzu, die einfach prüft, ob das übergebene Glossar einen Schlüssel enthält, der mit dem Namen des aktuellen Handelssymbols übereinstimmt, und das Handelsinstrument gegebenenfalls ändert:
//+------------------------------------------------------------------+ //| Trading strategy using tick volumes | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { protected: string m_symbol; // Symbol (trading instrument) ... public: ... // Replace symbol names virtual bool SymbolsReplace(CHashMap<string, string> &p_symbolsMap); }; ... //+------------------------------------------------------------------+ //| Replace symbol names | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { // If there is a key in the glossary that matches the current symbol if(p_symbolsMap.ContainsKey(m_symbol)) { string targetSymbol; // Target symbol // If the target symbol for the current one is successfully retrieved from the glossary if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) { // Update the current symbol m_symbol = targetSymbol; } } return true; }
Wir speichern die Änderungen in der Datei SimpleVoumesStrategy.mqh im aktuellen Ordner.
Damit sind die Bearbeitungen zu dieser Teilaufgabe abgeschlossen. Die Prüfung des Testers hat gezeigt, dass der EA erfolgreich den Handel mit neuen Symbolen in Übereinstimmung mit den Substitutionsregeln aufnimmt. Da wir die Methode CHashMap::Add() verwenden, um das Substitutionsglossar zu füllen, führt der Versuch, ein neues Element (Zielsymbol) mit einem bereits vorhandenen Schlüssel (Quellsymbol) hinzuzufügen, zu einem Fehler.
Das bedeutet, dass der EA die Initialisierung nicht bestehen wird, wenn wir die Substitutionsregel für dasselbe Symbol zweimal in der Substitutionszeichenfolge angeben. Der Substitutionsstring muss angepasst werden, wobei die Wiederholung von Substitutionsregeln für dieselben Handelssymbole auszuschließen ist.
Modus für den Abschluss des Handels
Der nächste Punkt, den wir geplant haben, ist die Möglichkeit, einen speziellen Modus des Expert Advisors einzustellen — das Handelsende. Zunächst müssen wir uns darauf einigen, was wir darunter verstehen. Da wir diesen Modus nur dann aktivieren wollen, wenn wir einen neuen EA mit anderen Parametern anstelle des bereits laufenden EAs starten wollen, sind wir einerseits daran interessiert, alle vom alten EA eröffneten Positionen so schnell wie möglich zu schließen. Andererseits würden wir Positionen nicht schließen wollen, wenn ihr laufender Gewinn aktuell negativ ist. In diesem Fall ist es vielleicht besser, eine Weile zu warten, bis der EA aus dem Drawdown herauskommt.
Daher formulieren wir das Problem wie folgt: Wenn der Modus für das Handelsende aktiviert ist, sollte der EA alle Positionen schließen und keine neuen eröffnen, sobald der gleitende Gewinn nicht negativ ist. Wenn der Gewinn in dem Moment, in dem dieser Modus eingeschaltet wird, nicht negativ ist, müssen wir überhaupt nicht warten — der EA schließt alle Positionen sofort. Wenn nicht, dann müssen wir warten.
Die nächste Frage ist, wie lange wir noch warten müssen. Betrachtet man die Ergebnisse historischer Tests, so kann man feststellen, dass die Rückgänge mehrere Monate andauerten. Wenn wir also einfach abwarten, kann sich die Wartezeit ziemlich lange hinziehen, wenn der Start des Handelsabschlussmodus nicht erfolgreich ist. Vielleicht wäre es profitabler, alle Positionen der alten Version zu schließen, ohne auf Gewinne zu warten, d.h. die aktuellen Verluste zu akzeptieren. Auf diese Weise könnte die neue Version schneller in Betrieb genommen werden, sodass in der Zeit, in der die alte Version in die Gewinnzone kommt, möglicherweise ein Gewinn erzielt wird, der die bei der Einstellung der alten Version in Kauf genommenen Verluste ausgleicht.
Wir können jedoch weder den Zeitpunkt des Ausstiegs der alten Version aus dem Drawdown noch den potenziellen Gewinn der neuen Version während dieses Zeitraums im Voraus kennen, da diese Ergebnisse zum Zeitpunkt der Entscheidungsfindung in der Zukunft liegen.
Ein möglicher Kompromiss in dieser Situation könnte darin bestehen, eine maximale Wartezeit einzuführen, nach deren Ablauf alle Positionen der alten Version bei jeder aktuellen Inanspruchnahme zwangsweise geschlossen werden. Wir können uns komplexere Optionen einfallen lassen. Wir können dieses Zeitlimit beispielsweise als Parameter einer linearen oder nichtlinearen Zeitfunktion verwenden, die den Betrag liefert, zu dem wir bereit sind, alle Positionen zu schließen. Im einfachsten Fall handelt es sich um eine Schwellenwertfunktion, die vor dieser Zeitspanne 0 zurückgibt. Danach wird ein Wert zurückgegeben, der geringer ist als das aktuelle Guthaben auf dem Konto. Dies führt zu einer garantierten Schließung aller Positionen nach der angegebenen Zeit.
Lassen Sie uns mit der Implementierung fortfahren. Die erste Möglichkeit war, zwei Eingaben (Schließmodus aktivieren und Zeit in Tagen begrenzen) in die EA-Datei einzufügen und sie dann in der Initialisierungsfunktion weiter zu verwenden:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input bool useOnlyCloseMode_ = false; // - Enable close mode input double onlyCloseDays_ = 0; // - Maximum time of closing mode (days) ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Prepare the initialization string for an EA with a group of several strategies string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%.2f" " )\n" " ,%d,%s,%d\n" " ,%d,%.2f\n" ")", strategiesParams, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "SimpleVolumes", useOnlyNewBars_, useOnlyCloseMode_, onlyCloseDays_ ); // Create an EA handling virtual positions expert = NEW(expertParams); ... // Successful initialization return(INIT_SUCCEEDED); }
Im weiteren Verlauf wurde jedoch klar, dass wir einen Code schreiben mussten, der dem, was wir vor kurzem noch gemacht hatten, sehr ähnlich war. Wie sich herausstellte, ist das erforderliche Verhalten im Beendungsmodus dem Verhalten eines EA sehr ähnlich, dessen Risikomanager einen Zielgewinnwert hat, der der Differenz zwischen dem aktuellen Saldo zu Beginn des Beendungsmodus und dem Basissaldo entspricht. Warum also nicht den Risikomanager ein wenig modifizieren, sodass der Modus für das Beenden einfach durch die Einstellung der erforderlichen Parameter im Risikomanager implementiert werden kann?
Lassen Sie uns darüber nachdenken, was uns beim Risikomanager fehlt, um die Arbeit im Abschlussmodus umzusetzen. Im einfachsten Fall, wenn wir uns nicht mit der Höchstdauer befassen, muss der Risikomanager nicht überarbeitet werden. In der alten Version mussten wir lediglich den Parameter für den Zielgewinn auf einen Wert setzen, der der Differenz zwischen dem aktuellen Kontostand und dem Basissaldo entsprach, und abwarten, bis dieser Wert erreicht war. Wir können sogar noch weiter gehen und sie im Laufe der Zeit regelmäßig ändern. Es ist zu erwarten, dass dieser Mechanismus nur sehr selten zum Einsatz kommt. Ein automatisches Beenden nach der festgelegten Zeit wäre jedoch wünschenswert. Daher sollten wir dem Risikomanager die Möglichkeit geben, nicht nur den Zielgewinn, sondern auch die maximal zulässige Wartezeit festzulegen. Sie wird die Rolle der maximalen Zeit für das Schließen von Positionen spielen.
Es ist für uns bequemer, diese Zeit in Form eines bestimmten Datums und einer Uhrzeit zu übermitteln, sodass wir uns nicht mehr an das Datum des Arbeitsbeginns erinnern müssen, ab dem das angegebene Intervall gezählt werden soll. Fügen wir diesen Parameter der Reihe von Eingaben in Bezug auf den Risikomanager hinzu. Wir werden auch die Ersetzung seines Wertes in die Initialisierungszeichenfolge aufnehmen:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input group "::: Risk manager" ... input ENUM_RM_CALC_OVERALL_PROFIT rmCalcOverallProfitLimit_ = RM_CALC_OVERALL_PROFIT_MONEY_BB; // - Method for calculating total profit input double rmMaxOverallProfitLimit_ = 1000000; // - Overall profit input datetime rmMaxOverallProfitDate_ = 0; // - Maximum time of waiting for the total profit (days) ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Prepare the initialization string for an EA with a group of several strategies string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %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" ")", strategiesParams, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,rmMaxOverallProfitDate_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "SimpleVolumes", useOnlyNewBars_ ); // Create an EA handling virtual positions expert = NEW(expertParams); ... // Successful initialization return(INIT_SUCCEEDED); }
Wir speichern die Änderungen in der Datei SimpleVolumesExpert.mq5 im aktuellen Ordner.
Wir fügen in der Risikomanager-Klasse zunächst eine neue Eigenschaft für die maximale Wartezeit auf einen bestimmten Gewinn hinzu und setzen deren Wert im Konstruktor aus dem Initialisierungsstring. Außerdem fügen wir die neue Methode OverallProfit() hinzu, die den gewünschten Gewinnwert für den Abschluss zurückgibt:
//+------------------------------------------------------------------+ //| Risk management class (risk manager) | //+------------------------------------------------------------------+ class CVirtualRiskManager : public CFactorable { protected: // Main constructor parameters ... ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit double m_maxOverallProfitLimit; // Parameter for calculating the maximum overall profit datetime m_maxOverallProfitDate; // Maximum time of reaching the total profit ... // Protected methods double DailyLoss(); // Maximum daily loss double OverallLoss(); // Maximum total loss double OverallProfit(); // Maximum profit ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualRiskManager::CVirtualRiskManager(string p_params) { // Save the initialization string m_params = p_params; // Read the initialization string and set the property values m_isActive = (bool) ReadLong(p_params); m_baseBalance = ReadDouble(p_params); m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params); m_maxDailyLossLimit = ReadDouble(p_params); m_closeDailyPart = ReadDouble(p_params); m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params); m_maxOverallLossLimit = ReadDouble(p_params); m_closeOverallPart = ReadDouble(p_params); m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params); m_maxOverallProfitLimit = ReadDouble(p_params); m_maxOverallProfitDate = (datetime) ReadLong(p_params); m_maxRestoreTime = ReadDouble(p_params); m_lastVirtualProfitFactor = ReadDouble(p_params); ... }
Die Methode OverallProfit() prüft zunächst, ob die Zeit zum Erreichen des gewünschten Gewinns festgelegt ist. Wenn die Zeit eingestellt ist und die aktuelle Zeit die eingestellte Zeit bereits überschritten hat, dann gibt die Methode den aktuellen Gewinnwert zurück, da der aktuelle Wert bereits ein gewünschter Wert ist. Dies wird schließlich dazu führen, dass alle Positionen geschlossen und der Handel eingestellt wird. Wenn die Zeit noch nicht erreicht ist, gibt die Methode den aus der Eingabe errechneten Wert des gewünschten Gewinns zurück:
//+------------------------------------------------------------------+ //| Maximum total profit | //+------------------------------------------------------------------+ double CVirtualRiskManager::OverallProfit() { // Current time datetime tc = TimeCurrent(); // If the current time is greater than the specified maximum time, if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) { // Return the value that guarantees the positions are closed return m_overallProfit; } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) { // To get a given percentage of the base balance, calculate it return m_baseBalance * m_maxOverallProfitLimit / 100; } else { // To get a fixed value, just return it // RM_CALC_OVERALL_PROFIT_MONEY_BB return m_maxOverallProfitLimit; } }
Wir werden diese Methode verwenden, wenn wir die Notwendigkeit der Schließung innerhalb der Methode CheckOverallProfitLimit() überprüfen:
//+------------------------------------------------------------------+ //| Check if the specified profit has been achieved | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckOverallProfitLimit() { // If overall loss is reached and positions are still open if(m_overallProfit >= OverallProfit() && CMoney::DepoPart() > 0) { // Reduce the multiplier of the used part of the overall balance by the overall loss m_overallDepoPart = 0; // Set the risk manager to the achieved overall profit state m_state = RM_STATE_OVERALL_PROFIT; // Set the value of the used part of the overall balance SetDepoPart(); ... return true; } return false; }
Speichern wir die Änderungen in der Datei VirtualRiskManager.mqh-Datei im aktuellen Ordner.
Die Änderungen in Bezug auf den Abschaltmodus sind weitgehend abgeschlossen. Den Rest werden wir zu einem späteren Zeitpunkt hinzufügen, um sicherzustellen, dass die Funktionalität nach einem Neustart wiederhergestellt werden kann.
Wiederherstellen nach einem Neustart
Die Notwendigkeit, eine solche Möglichkeit zu schaffen, wurde bereits in den ersten Teilen der Reihe ins Auge gefasst. Viele der von uns erstellten Klassen verfügen bereits über die Methoden Save() und Load(), die speziell für das Speichern und Laden des Objektstatus vorgesehen sind. Bei einigen dieser Methoden hatten wir bereits funktionierenden Code, aber dann waren wir mit anderen Dingen beschäftigt und haben diese Methoden nicht mehr richtig funktionieren lassen, da dies unnötig war. Es ist an der Zeit, sich auf sie zu konzentrieren und sie wieder zum Laufen zu bringen.
Die wichtigsten Änderungen werden wir wohl wieder in der Risikomanager-Klasse vornehmen müssen, da diese Methoden dort noch völlig leer sind. Außerdem müssen wir sicherstellen, dass die Methoden zum Speichern und Laden des Risikomanagers beim Laden/Speichern des EA aufgerufen werden, da der Risikomanager später erschien und nicht zu den gespeicherten Informationen hinzugefügt wurde.
Beginnen wir damit, dem EA eine Eingabe hinzuzufügen, die bestimmt, ob der vorherige Zustand wiederhergestellt werden soll. Standardmäßig ist dies True. Wenn wir den EA von Grund auf neu starten wollen, können wir ihn auf False setzen, den EA neu starten (in diesem Fall werden alle zuvor gespeicherten Informationen mit neuen überschrieben) und dann diesen Parameter wieder auf True setzen. In der EA-Initialisierungsfunktion prüfen wir, ob der vorherige Zustand geladen werden muss, und wenn ja, laden wir ihn:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input bool usePrevState_ = true; // - Load the previous state ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Create an EA handling virtual positions expert = NEW(expertParams); // If the EA is not created, then return an error if(!expert) return INIT_FAILED; // If an error occurred while replacing symbols, then return an error if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED; // If we need to restore the state, if(usePrevState_) { // Load the previous state if available expert.Load(); expert.Tick(); } // Successful initialization return(INIT_SUCCEEDED); }
Wir speichern die Änderungen in der Datei SimpleVolumesExpert.mq5 Datei im aktuellen Ordner.
Bevor wir zu den Methoden des Speicherns/Ladens des Zustands übergehen, sollten wir den folgenden Aspekt beachten. In der vorherigen Version wurde der Dateiname zum Speichern aus dem Namen des EA, seiner magischen Nummer und dem Wort „.test“ gebildet, wenn er im visuellen Testmodus ausgeführt wurde. Der EA-Name ist ein konstanter Wert. Er ist in den Quellcode eingebettet und wird nicht über die EA-Eingaben verändert. Die magische Zahl kann über die Eingänge verändert werden. Das heißt, wenn wir die magische Zahl ändern, lädt der EA nicht mehr die Datei, die mit der zuvor verwendeten magischen Zahl erzeugt wurde. Das bedeutet aber auch, dass, wenn wir die Zusammensetzung der einzelnen Instanzen der Handelsstrategien ändern, aber die gleiche magische Zahl beibehalten, der EA versuchen wird, die vorherige Datei zum Laden des Status zu verwenden.
Dies wird höchstwahrscheinlich zu Fehlern führen, sodass wir uns vor einer solchen Situation schützen müssen. Eine Möglichkeit besteht darin, einen Teil in den Dateinamen aufzunehmen, der von den verwendeten Handelsstrategie-Instanzen abhängt. Wenn sich ihre Zusammensetzung ändert, dann ändert sich auch dieser Teil des Dateinamens, was bedeutet, dass der EA nach der Aktualisierung der Zusammensetzung der Strategien nicht mehr die alte Datei verwendet.
Es ist möglich, einen solchen sich ändernden Teil des Dateinamens zu bilden, indem eine Hash-Funktion aus dem EA-Initialisierungsstring oder einem Teil davon berechnet wird. Wir haben bereits darauf hingewiesen, dass die Verwendung einer anderen Datei nur dann erforderlich ist, wenn die Zusammensetzung der Handelsstrategien geändert wird. Wenn wir z.B. die Einstellungen des Risikomanagers ändern, ändert sich zwar der Initialisierungsstring, aber der Name der Datei, in der der Status gespeichert wird, sollte sich nicht ändern. Daher berechnen wir die Hash-Funktion nur aus dem Teil des Initialisierungsstrings, der Informationen über einzelne Instanzen von Handelsstrategien enthält.
Dazu fügen wir die neue Methode HashParams() hinzu und nehmen Änderungen am EA-Konstruktor vor:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... virtual string HashParams(string p_name); // Hash value of EA parameters public: ... }; ... //+------------------------------------------------------------------+ //| Hash value of EA parameters | //+------------------------------------------------------------------+ string CVirtualAdvisor::HashParams(string p_params) { uchar hash[], key[], data[]; // Calculate the hash from the initialization string StringToCharArray(p_params, data); CryptEncode(CRYPT_HASH_MD5, data, key, hash); // Convert it from the array of numbers to a string with hexadecimal notation string res = ""; FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-"); return res; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { ... // If there are no read errors, if(IsValid()) { ... // Form the name of the file for saving the state from the EA name and parameters m_name = StringFormat("%s-%d-%s%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, HashParams(groupParams), (MQLInfoInteger(MQL_TESTER) ? ".test" : "") );; ... } }
Hinzufügen des Speicherns/Ladens des Risikomanagers zu den entsprechenden EA-Methoden:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Save() { bool res = true; // Save status if: if(true // later changes appeared && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t'); if(f != INVALID_HANDLE) { // If file is open, save FileWrite(f, CVirtualReceiver::s_lastChangeTime); // Time of last changes // All strategies FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f)); m_riskManager.Save(f); FileClose(f); // Update the last save time m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } return res; } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Load() { bool res = true; // Load status if: if(true // file exists && FileIsExist(m_name) // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t'); if(f != INVALID_HANDLE) { // If the file is open, then load m_lastSaveTime = FileReadDatetime(f); // Last save time PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); // Load all strategies FOREACH(m_strategies, { res &= ((CVirtualStrategy*) m_strategies[i]).Load(f); if(!res) break; }); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name); } res &= m_riskManager.Load(f); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name); } FileClose(f); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } return res; }
Wir speichern die Änderungen in der Datei VirtualAdvisor.mq5 Datei im aktuellen Ordner.
Jetzt müssen wir nur noch die Implementierung der Methoden von zum Speichern/Laden des Risikomanagers schreiben. Schauen wir uns an, was wir für den Risikomanager einsparen müssen. Die Eingaben des Risikomanagers müssen nicht gespeichert werden — sie werden immer von den Eingaben des EAs übernommen. Beim nächsten Start können die geänderten Werte bereits ersetzt werden. Es besteht auch keine Notwendigkeit, die vom Risikomanager selbst aktualisierten Werte zu speichern: Saldo, Eigenkapital, Tagesgewinn und andere. Das Einzige, was man behalten sollte, ist der tägliche Grundwert, da er nur einmal am Tag berechnet wird.
Alle Eigenschaften, die sich auf den aktuellen Stand und die Verwaltung des Umfangs der offenen Positionen beziehen (mit Ausnahme des verbrauchten Teils des Gesamtsaldos), sollten beibehalten werden.
// Current state ENUM_RM_STATE m_state; // State double m_lastVirtualProfit; // Profit of open virtual positions at the moment of loss limit datetime m_startRestoreTime; // Start time of restoring the size of open positions datetime m_startTime; // Updated values ... // Managing the size of open positions double m_baseDepoPart; // Used (original) part of the total balance double m_dailyDepoPart; // Multiplier of the used part of the total balance by daily loss double m_overallDepoPart; // Multiplier of the used part of the total balance by overall loss
Unter Berücksichtigung der obigen Ausführungen könnte die Implementierung dieser Methoden wie folgt aussehen:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualRiskManager::Save(const int f) { FileWrite(f, m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime, m_dailyDepoPart, m_overallDepoPart); return true; } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualRiskManager::Load(const int f) { m_state = (ENUM_RM_STATE) FileReadNumber(f); m_lastVirtualProfit = FileReadNumber(f); m_startRestoreTime = FileReadDatetime(f); m_startTime = FileReadDatetime(f); m_dailyDepoPart = FileReadNumber(f); m_overallDepoPart = FileReadNumber(f); return true; }
Wir speichern die Änderungen in der Datei VirtualRiskManager.mq5 Datei im aktuellen Ordner.
Test
Um die zusätzlichen Funktionsweisen zu testen, werden wir zwei Wege gehen. Zunächst installieren wir den kompilierten EA auf dem Chart und stellen wir sicher, dass die Statusdatendatei erstellt wird. Für eine weitere Überprüfung müssen wir warten, bis der EA eine Position eröffnet. Aber darauf können wir ziemlich lange warten, und noch länger müssen wir warten, bis der Risikomanager ausgelöst wird, damit wir die Korrektheit der Wiederaufnahme der Arbeit durch den EA nach seinem Eingriff überprüfen können. Zweitens werden wir den Strategietester verwenden und die Situation simulieren, in der die Arbeit des EA nach dem Anhalten wieder aufgenommen wird.
Zu diesem Zweck erstellen wir einen neuen EA auf der Grundlage des bestehenden EA, dem wir zwei neue Eingaben hinzufügen: die Stoppzeit vor dem Neustart und die Startzeit des Neustarts. Sie werden wie folgt behandelt:
- Wenn die Stoppzeit vor dem Neustart nicht angegeben ist (gleich Null oder 1970-01-01 00:00:00) oder nicht in das Testintervall fällt, funktioniert der EA wie der ursprüngliche;
- wenn eine bestimmte Stoppzeit angegeben wird, die in das Testintervall fällt, dann stoppt der EA bei Erreichen dieser Zeit die Ausführung des Tick-Handlers für das EA-Objekt bis zu der im zweiten Parameter angegebenen Zeit.
Im Code sehen diese beiden Parameter wie folgt aus:
input datetime restartStopTime_ = 0; // - Stop time before restart input datetime restartStartTime_ = 0; // - Restart launch time
Nehmen wir die Änderungen an der Tick-Handling-Funktion im EA vor. Um zu erkennen, dass eine Unterbrechung stattgefunden hat, fügen wir die globale, boolesche Variable isRestarting hinzu. Wenn sie True ist, dann ist der EA gerade in Bereitschaft. Sobald die aktuelle Zeit die Wiederaufnahmezeit überschreitet, laden wir den vorherigen EA-Status und setzen das Flag isRestarting zurück:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // If the stop time is specified, if(restartStopTime_ != 0) { // Define the current time datetime tc = TimeCurrent(); // If we are in the interval between stopping and resuming, if(tc >= restartStopTime_ && tc <= restartStartTime_) { // Save the status and exit isRestarting = true; return; } // If we were in a state between stopping and resuming, // and it is time to resume work, if(isRestarting && tc > restartStartTime_) { // Load the EA status expert.Load(); // Reset the status flag between stop and resume isRestarting = false; } } // Perform tick handling expert.Tick(); }
Wir speichern die Änderungen in der Datei SimpleVolumesTestRestartExpert.mq5 im aktuellen Ordner.
Betrachten wir die Ergebnisse ohne Unterbrechung im Zeitraum 2021-2022.
Abb. 1. Testergebnisse ohne Unterbrechung des Handels
Lassen Sie uns nun eine kurze Pause von der Arbeit des EA einlegen. Nach dem Testlauf waren die Ergebnisse genau dieselben wie ohne Unterbrechung. Dies zeigt an, dass der EA nach einer kurzen Pause seinen Status erfolgreich wiederherstellt und weiterarbeitet.
Um den Unterschied zu sehen, sollten wir eine längere Pause einlegen, zum Beispiel 4 Monate. Wir erhalten die folgenden Ergebnisse:
Abb. 2. Testergebnisse mit einer Handelspause vom 27.07.2021 bis 29.11.2021
Im Diagramm wird die ungefähre Position des Bruchs durch ein gelb umrandetes Rechteck dargestellt. Zu diesem Zeitpunkt wurden die von der EA eröffneten Positionen aufgegeben. Doch dann wurde der EA wieder aktiv, nahm seine offenen Positionen auf und konnte insgesamt gute Ergebnisse erzielen. Daher kann die Möglichkeit, den EA-Status zu speichern und zu laden, ebenfalls als implementiert betrachtet werden.
Schlussfolgerung
Werfen wir noch einmal einen Blick auf die erzielten Ergebnisse. Wir haben bereits damit begonnen, unseren EA ernsthaft auf die Arbeit mit einem echten Konto vorzubereiten. Zu diesem Zweck wurden verschiedene Szenarien betrachtet, die nicht im Tester, sondern meist im realen Handel vorkommen.
Wir haben uns angesehen, wie man sicherstellt, dass der EA auf einem Konto funktioniert, auf dem die Namen der Handelssymbole etwas anders lauten als die, für die er optimiert wurde. Zu diesem Zweck wurde die Möglichkeit geschaffen, Symbolnamen zu ersetzen. Wir haben auch die Möglichkeit implementiert, einen Handel sanft zu beenden, wenn es notwendig ist, einen EA mit anderen Eingaben zu starten. Eine weitere wichtige Entwicklung war die Möglichkeit, den Status des EA zu speichern, um eine ordnungsgemäße Wiederaufnahme der Arbeit nach einem Neustart des Terminals zu gewährleisten.
Dies sind jedoch nicht alle Vorbereitungen, die bei der Installation eines EA für die Arbeit auf einem echten Handelskonto getroffen werden sollten. Wir würden gerne die Anzeige verschiedener Arten von Zusatzinformationen besser gestalten und die Anzeige von zusammenfassenden Daten über den aktuellen Zustand des EA auf dem Diagramm ermöglichen. Wichtiger ist jedoch die Änderung, die es dem EA ermöglicht, in Abwesenheit einer Datenbank mit Optimierungsergebnissen im Arbeitsordner des Terminals zu arbeiten. Im Moment können wir darauf nicht verzichten, da die Komponenten für die Bildung des EA-Initialisierungsstrings aus dieser Datenbank stammen. Auf diese Verbesserungen werden wir in den folgenden Artikeln eingehen.
Vielen Dank für Ihre Aufmerksamkeit! Bis bald!
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/15294





- 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.