Das MQL5-Kochbuch: Stresstests von Handelsstrategien unter Verwendung nutzerdefinierter Symbole
Einführung
Seit einiger Zeit verfügt das Terminal MetaTrader 5 über eine neue Funktion: die Möglichkeit, nutzerdefinierte Symbole zu erstellen. Jetzt können Algo-Händler in einigen Fällen selbst als Broker agieren, während sie keine Handelsserver benötigen und die Handelsumgebung so vollständig kontrollieren können. Allerdings beziehen sich "einige Fälle" hier nur auf den Testmodus. Natürlich können Aufträge für bnutzerdefinierte Symbole nicht von Brokern ausgeführt werden. Dennoch ermöglichen diese Funktionen Algo-Händlern, ihre Handelsstrategien genauer zu testen.
In diesem Artikel werden wir Bedingungen für solche Tests schaffen. Beginnen wir mit der Klasse der nutzerdefinierten Symbole.
1. Die Klasse für die nutzerdefinierten Symbole: CiCustomSymbol
Die Standardbibliothek bietet eine Klasse für den vereinfachten Zugriff auf die Symboleigenschaften. Es ist die Klasse CSymbolInfo. Tatsächlich führt die Klasse eine Zwischenfunktion aus: Auf Anfrage des Nutzers kommuniziert die Klasse mit dem Server und erhält von ihm eine Antwort in Form eines Wertes der angeforderten Eigenschaft.
Unser Ziel ist es, eine ähnliche Klasse für ein nutzerdefiniertes Symbol zu erstellen. Außerdem wird diese Klassenfunktionalität noch umfangreicher sein, da wir Erstellung, Löschung und einige andere Methoden hinzufügen müssen. Auf der anderen Seite werden wir keine Methoden verwenden, die mit der Verbindung zum Server verbunden sind. Dazu gehören Refresh(), IsSynchronized() etc.
Diese Klasse zur Schaffung einer Handelsumgebung kapselt Standardfunktionen für die Arbeit mit nutzerdefinierten Symbolen.
Die Struktur der Klassendeklaration ist unten dargestellt.
//+------------------------------------------------------------------+ //| Class CiCustomSymbol. | //| Purpose: Base class for a custom symbol. | //+------------------------------------------------------------------+ class CiCustomSymbol : public CObject { //--- === Data members === --- private: string m_name; string m_path; MqlTick m_tick; ulong m_from_msc; ulong m_to_msc; uint m_batch_size; bool m_is_selected; //--- === Methods === --- public: //--- Constructor/Destructor void CiCustomSymbol(void); void ~CiCustomSymbol(void) {}; //--- create/delete int Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false); bool Delete(void); //--- methods of access to protected data string Name(void) const { return(m_name); } bool RefreshRates(void); //--- fast access methods to the integer symbol properties bool Select(void) const; bool Select(const bool select); //--- service methods bool Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0); bool LoadTicks(const string _src_file_name); bool ChangeSpread(const uint _spread_size,const uint _spread_markup=0, const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID); //--- API bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const; bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const; bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const; double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const; long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const; string GetProperty(ENUM_SYMBOL_INFO_STRING _property) const; bool SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); bool SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); int RatesDelete(const datetime _from,const datetime _to); int RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]); int RatesUpdate(const MqlRates &_rates[]) const; int TicksAdd(const MqlTick &_ticks[]) const; int TicksDelete(const long _from_msc,long _to_msc) const; int TicksReplace(const MqlTick &_ticks[]) const; //--- private: template<typename PT> bool CloneProperty(const string _origin_symbol,const PT _prop_type) const; int CloneTicks(const MqlTick &_ticks[]) const; int CloneTicks(const string _origin_symbol) const; }; //+------------------------------------------------------------------+
Beginnen wir mit den Methoden, die aus einem ausgewählten Symbol ein vollwertiges nutzerdefiniertes Symbol machen.
1.1 Die Methode CiCustomSymbol::Create()
Um alle Möglichkeiten der nutzerdefinierten Symbole nutzen zu können, müssen wir sie erstellen oder sicherstellen, dass sie erstellt wurden.
//+------------------------------------------------------------------+ //| Create a custom symbol | //| Codes: | //| -1 - failed to create; | //| 0 - a symbol exists, no need to create; | //| 1 - successfully created. | //+------------------------------------------------------------------+ int CiCustomSymbol::Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false) { int res_code=-1; m_name=m_path=NULL; if(_batch_size<1e2) { ::Print(__FUNCTION__+": a batch size must be greater than 100!"); } else { ::ResetLastError(); //--- attempt to create a custom symbol if(!::CustomSymbolCreate(_name,_path,_origin_name)) { if(::SymbolInfoInteger(_name,SYMBOL_CUSTOM)) { ::PrintFormat(__FUNCTION__+": a custom symbol \"%s\" already exists!",_name); res_code=0; } else { ::PrintFormat(__FUNCTION__+": failed to create a custom symbol. Error code: %d",::GetLastError()); } } else res_code=1; if(res_code>=0) { m_name=_name; m_path=_path; m_batch_size=_batch_size; //--- if the custom symbol must be selected in the "Market Watch" if(_is_selected) { if(!this.Select()) if(!this.Select(true)) { ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } } else { if(this.Select()) if(!this.Select(false)) { ::PrintFormat(__FUNCTION__+": failed to unset the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } } m_is_selected=_is_selected; } } //--- return res_code; } //+------------------------------------------------------------------+
Das Verfahren gibt einen Wert als Zahlencode zurück:
- -1 — Fehler beim Erstellen eines Symbols;
- 0 — das Symbol wurde früher erstellt;
- 1 — das Symbol wurde beim aktuellen Methodenaufruf erfolgreich erzeugt.
Hier sind ein paar Worte zu den Parametern _batch_size und _is_selected.
Der erste Parameter (_batch_size) legt die Größe des Batches fest, der zum Laden von Ticks verwendet wird. Die Ticks werden in Batches geladen: Daten werden zuerst in ein Hilfsarray gelesen; sobald das Array gefüllt ist, werden die Daten in die Tick-Datenbank eines nutzerdefinierten Symbols geladen (Tick-Historie). Auf der einen Seite müssen Sie mit diesem Ansatz kein riesiges Array erstellen, auf der anderen Seite brauchen Sie die Tick-Datenbank nicht zu oft zu aktualisieren. Die Standardgröße des Hilfs-Tick-Arrays ist 1 Million.
Der zweite Parameter (_is_selected) bestimmt, ob wir die Ticks direkt in die Datenbank schreiben oder ob sie zuerst dem Fenster Marktübersicht hinzugefügt werden.
Als Beispiel führen wir das Skript TestCreation.mql5 aus, das ein eigenes Symbol erstellt.
Der von diesem Methodenaufruf zurückgegebene Code wird im Journal angezeigt.
2019.08.11 12:34:08.055 TestCreation (EURUSD,M1) A custom symbol "EURUSD_1" creation has returned the code: 1
Für weitere Details zur Erstellung nutzerdefinierter Symbole lesen Sie bitte die Dokumentation.
1.2 Die Methode CiCustomSymbol::Delete()
Diese Methode versucht, ein nutzerdefiniertes Symbol zu löschen. Bevor das Symbol gelöscht wird, versucht die Methode, es aus dem Fenster Marktübersicht zu entfernen. Im Falle eines Fehlers wird der Löschvorgang unterbrochen.
//+------------------------------------------------------------------+ //| Delete | //+------------------------------------------------------------------+ bool CiCustomSymbol::Delete(void) { ::ResetLastError(); if(this.Select()) if(!this.Select(false)) { ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } if(!::CustomSymbolDelete(m_name)) { ::PrintFormat(__FUNCTION__+": failed to delete the custom symbol \"%s\". Error code: %d",m_name,::GetLastError()); return false; } //--- return true; } //+------------------------------------------------------------------+
Als Beispiel starten wir ein einfaches Skript TestDeletion.mql5, das ein eigenes Symbol löscht. Bei Erfolg wird dem Journal ein entsprechender Eintrag hinzugefügt.
2019.08.11 19:13:59.276 TestDeletion (EURUSD,M1) A custom symbol "EURUSD_1" has been successfully deleted.
1.3 Die Methode CiCustomSymbol::Clone()
Diese Methode führt das Klonen durch: Basierend auf dem ausgewählten Symbol bestimmt sie die Eigenschaften für das aktuelle nutzerdefinierte Symbol. Einfach ausgedrückt, erhalten wir den Eigenschaftswert des ursprünglichen Symbols und kopieren es in ein anderes Symbol. Der Nutzer kann auch das Klonen einer Tick-Historie einstellen. Dazu müssen Sie das Zeitintervall definieren.
//+------------------------------------------------------------------+ //| Clone a symbol | //+------------------------------------------------------------------+ bool CiCustomSymbol::Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0) { if(!::StringCompare(m_name,_origin_symbol)) { ::Print(__FUNCTION__+": the origin symbol name must be different!"); return false; } ::ResetLastError(); //--- if to load history if(_to_msc>0) { if(_to_msc<_from_msc) { ::Print(__FUNCTION__+": wrong settings for a time interval!"); return false; } m_from_msc=_from_msc; m_to_msc=_to_msc; } else m_from_msc=m_to_msc=0; //--- double ENUM_SYMBOL_INFO_DOUBLE dbl_props[]= { SYMBOL_MARGIN_HEDGED, SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_OPTION_STRIKE, SYMBOL_POINT, SYMBOL_SESSION_PRICE_LIMIT_MAX, SYMBOL_SESSION_PRICE_LIMIT_MIN, SYMBOL_SESSION_PRICE_SETTLEMENT, SYMBOL_SWAP_LONG, SYMBOL_SWAP_SHORT, SYMBOL_TRADE_ACCRUED_INTEREST, SYMBOL_TRADE_CONTRACT_SIZE, SYMBOL_TRADE_FACE_VALUE, SYMBOL_TRADE_LIQUIDITY_RATE, SYMBOL_TRADE_TICK_SIZE, SYMBOL_TRADE_TICK_VALUE, SYMBOL_VOLUME_LIMIT, SYMBOL_VOLUME_MAX, SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_STEP }; for(int prop_idx=0; prop_idx<::ArraySize(dbl_props); prop_idx++) { ENUM_SYMBOL_INFO_DOUBLE curr_property=dbl_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- integer ENUM_SYMBOL_INFO_INTEGER int_props[]= { SYMBOL_BACKGROUND_COLOR, SYMBOL_CHART_MODE, SYMBOL_DIGITS, SYMBOL_EXPIRATION_MODE, SYMBOL_EXPIRATION_TIME, SYMBOL_FILLING_MODE, SYMBOL_MARGIN_HEDGED_USE_LEG, SYMBOL_OPTION_MODE, SYMBOL_OPTION_RIGHT, SYMBOL_ORDER_GTC_MODE, SYMBOL_ORDER_MODE, SYMBOL_SPREAD, SYMBOL_SPREAD_FLOAT, SYMBOL_START_TIME, SYMBOL_SWAP_MODE, SYMBOL_SWAP_ROLLOVER3DAYS, SYMBOL_TICKS_BOOKDEPTH, SYMBOL_TRADE_CALC_MODE, SYMBOL_TRADE_EXEMODE, SYMBOL_TRADE_FREEZE_LEVEL, SYMBOL_TRADE_MODE, SYMBOL_TRADE_STOPS_LEVEL }; for(int prop_idx=0; prop_idx<::ArraySize(int_props); prop_idx++) { ENUM_SYMBOL_INFO_INTEGER curr_property=int_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- string ENUM_SYMBOL_INFO_STRING str_props[]= { SYMBOL_BASIS, SYMBOL_CURRENCY_BASE, SYMBOL_CURRENCY_MARGIN, SYMBOL_CURRENCY_PROFIT, SYMBOL_DESCRIPTION, SYMBOL_FORMULA, SYMBOL_ISIN, SYMBOL_PAGE, SYMBOL_PATH }; for(int prop_idx=0; prop_idx<::ArraySize(str_props); prop_idx++) { ENUM_SYMBOL_INFO_STRING curr_property=str_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- history if(_to_msc>0) { if(this.CloneTicks(_origin_symbol)==-1) return false; } //--- return true; } //+------------------------------------------------------------------+
Bitte beachten Sie, dass nicht alle Eigenschaften kopiert werden können, da einige von ihnen auf Terminalebene eingestellt sind. Ihre Werte können abgerufen werden, aber sie können nicht kontrolliert werden ( get-Eigenschaften).
Ein Versuch, eine get-Eigenschaft auf ein nutzerdefiniertes Symbol zu setzen, gibt den Fehler 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG) zurück. Darüber hinaus gibt es unter dem Abschnitt "Laufzeitfehler" eine eigene Fehlergruppe für nutzerdefinierte Symbole.
Als Beispiel führen wir ein einfaches Skript TestClone.mql5 aus, das ein einfaches Symbol klont. Wenn der Versuch zu klonen erfolgreich war, erscheint das folgende Protokoll im Journal.
2019.08.11 19:21:06.402 TestClone (EURUSD,M1) A base symbol "EURUSD" has been successfully cloned.
1.4 Die Methode CiCustomSymbol::LoadTicks()
Diese Methode liest Ticks aus der Datei und lädt sie zur weiteren Verwendung. Beachten Sie, dass die Methode zuvor die bestehende Tick-Datenbank für dieses nutzerdefinierte Symbol löscht.
//+------------------------------------------------------------------+ //| Load ticks | //+------------------------------------------------------------------+ bool CiCustomSymbol::LoadTicks(const string _src_file_name) { int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);; //--- delete ticks if(this.TicksDelete(0,LONG_MAX)<0) return false; //--- open a file CFile curr_file; ::ResetLastError(); int file_ha=curr_file.Open(_src_file_name,FILE_READ|FILE_CSV,','); if(file_ha==INVALID_HANDLE) { ::PrintFormat(__FUNCTION__+": failed to open a %s file!",_src_file_name); return false; } curr_file.Seek(0,SEEK_SET); //--- read data from a file MqlTick batch_arr[]; if(::ArrayResize(batch_arr,m_batch_size)!=m_batch_size) { ::Print(__FUNCTION__+": failed to allocate memory for a batch array!"); return false; } ::ZeroMemory(batch_arr); uint tick_idx=0; bool is_file_ending=false; uint tick_cnt=0; do { is_file_ending=curr_file.IsEnding(); string dates_str[2]; if(!is_file_ending) { //--- time string time_str=::FileReadString(file_ha); if(::StringLen(time_str)<1) { ::Print(__FUNCTION__+": no datetime string - the current tick skipped!"); ::PrintFormat("The unprocessed string: %s",time_str); continue; } string sep="."; ushort u_sep; string result[]; u_sep=::StringGetCharacter(sep,0); int str_num=::StringSplit(time_str,u_sep,result); if(str_num!=4) { ::Print(__FUNCTION__+": no substrings - the current tick skipped!"); ::PrintFormat("The unprocessed string: %s",time_str); continue; } //--- datetime datetime date_time=::StringToTime(result[0]+"."+result[1]+"."+result[2]); long time_msc=(long)(1e3*date_time+::StringToInteger(result[3])); //--- bid double bid_val=::FileReadNumber(file_ha); if(bid_val<.0) { ::Print(__FUNCTION__+": no bid price - the current tick skipped!"); continue; } //--- ask double ask_val=::FileReadNumber(file_ha); if(ask_val<.0) { ::Print(__FUNCTION__+": no ask price - the current tick skipped!"); continue; } //--- volumes for(int jtx=0; jtx<2; jtx++) ::FileReadNumber(file_ha); //--- fill in the current tick MqlTick curr_tick= {0}; curr_tick.time=date_time; curr_tick.time_msc=(long)(1e3*date_time+::StringToInteger(result[3])); curr_tick.bid=::NormalizeDouble(bid_val,symbol_digs); curr_tick.ask=::NormalizeDouble(ask_val,symbol_digs); //--- flags if(m_tick.bid!=curr_tick.bid) curr_tick.flags|=TICK_FLAG_BID; if(m_tick.ask!=curr_tick.ask) curr_tick.flags|=TICK_FLAG_ASK; if(curr_tick.flags==0) curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;; if(tick_idx==m_batch_size) { //--- add ticks to the custom symbol if(m_is_selected) { if(this.TicksAdd(batch_arr)!=m_batch_size) return false; } else { if(this.TicksReplace(batch_arr)!=m_batch_size) return false; } tick_cnt+=m_batch_size; //--- log for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(m_batch_size-1)) dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS); ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]); //--- reset ::ZeroMemory(batch_arr); tick_idx=0; } batch_arr[tick_idx]=curr_tick; m_tick=curr_tick; tick_idx++; } //--- end of file else { uint new_size=tick_idx; if(new_size>0) { MqlTick last_batch_arr[]; if(::ArrayCopy(last_batch_arr,batch_arr,0,0,new_size)!=new_size) { ::Print(__FUNCTION__+": failed to copy a batch array!"); return false; } //--- add ticks to the custom symbol if(m_is_selected) { if(this.TicksAdd(last_batch_arr)!=new_size) return false; } else { if(this.TicksReplace(last_batch_arr)!=new_size) return false; } tick_cnt+=new_size; //--- log for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(tick_idx-1)) dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS); ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]); } } } while(!is_file_ending && !::IsStopped()); ::PrintFormat("\nLoaded ticks number: %I32u",tick_cnt); curr_file.Close(); //--- MqlTick ticks_arr[]; if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,1,1)!=1) { ::Print(__FUNCTION__+": failed to copy the first tick!"); return false; } m_from_msc=ticks_arr[0].time_msc; if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,0,1)!=1) { ::Print(__FUNCTION__+": failed to copy the last tick!"); return false; } m_to_msc=ticks_arr[0].time_msc; //--- return true; } //+------------------------------------------------------------------+
In dieser Variante werden die folgenden Strukturfelder der Ticks gefüllt :
struct MqlTick { datetime time; // Last price update time double bid; // Current Bid price double ask; // Current Ask price double last; // Current price of the last trade (Last) ulong volume; // Volume for the current Last price long time_msc; // Last price update time in milliseconds uint flags; // Tick flags double volume_real; // Volume for the current Last price };
Der Wert TICK_FLAG_BID|TICK_FLAG_ASK wird für das erste Tick-Flag gesetzt. Der weitere Wert hängt davon ab, welcher Preis (Bid oder Ask) sich geändert hat. Hat sich keiner der Preise geändert, so werden sie als erster Tick verarbeitet.
Beginnend mit build 2085 kann eine Historie der Balken durch einfaches Laden der Tick-Historie erstellt werden. Wenn die Historie geladen wurde, können wir die Historie der Balken programmgesteuert abfragen.
Als Beispiel führen wir das einfache Skript TestLoad.mql5 aus, das Ticks aus einer Datei lädt. Die Datendatei muss sich unter dem Ordner %MQL5/Files befinden. In diesem Beispiel lautet die Datei EURUSD1_tick.csv. Es enthält EURUSD-Ticks für den 1. und 2. August 2019. Weiterhin werden wir uns mit Tick-Datenquellen befassen.
Nach dem Ausführen des Skripts wird die Zahl der geladenen Ticks im Journal angezeigt. Darüber hinaus werden wir die Anzahl der verfügbaren Ticks überprüfen, indem wir Daten aus der Tick-Datenbank des Terminals anfordern. 354.400 Ticks wurden kopiert. Somit sind die Zahlen gleich. Wir erhielten auch 2.697 1-Minuten-Balken.
NO 0 15:52:50.149 TestLoad (EURUSD,H1) LN 0 15:52:50.150 TestLoad (EURUSD,H1) Ticks loaded from 2019.08.01 00:00:00 to 2019.08.02 20:59:56. FM 0 15:52:50.152 TestLoad (EURUSD,H1) RM 0 15:52:50.152 TestLoad (EURUSD,H1) Loaded ticks number: 354400 EJ 0 15:52:50.160 TestLoad (EURUSD,H1) Ticks from the file "EURUSD1_tick.csv" have been successfully loaded. DD 0 15:52:50.170 TestLoad (EURUSD,H1) Copied 1-minute rates number: 2697 GL 0 15:52:50.170 TestLoad (EURUSD,H1) The 1st rate time: 2019.08.01 00:00 EQ 0 15:52:50.170 TestLoad (EURUSD,H1) The last rate time: 2019.08.02 20:56 DJ 0 15:52:50.351 TestLoad (EURUSD,H1) Copied ticks number: 354400
Andere Methoden gehören zur Gruppe der API-Methoden.
2. Tick-Daten, Quellen
Die Tick-Daten bilden eine Preisreihe, die ein sehr aktives Leben hat, in dem Angebot und Nachfrage miteinander kämpfen.
Die Art der Preisreihen ist seit langem Gegenstand von Diskussionen zwischen Händlern und Experten. Diese Serie ist Gegenstand der Analyse, die zugrunde liegende Grundlage für Entscheidungen usw.
Die folgenden Ideen sind im Artikelkontext anwendbar.
Wie Sie wissen, ist der Forex-Markt ein Freiverkehrsmarkt. Daher gibt es keine Vergleichs-Kurse. Dadurch gibt es keine Tick-Archive (Tick-Historie), die als Referenz dienen können. Aber es gibt Währungsfutures, von denen die meisten an der Chicago Mercantile Exchange gehandelt werden. Wahrscheinlich können diese Angebote als eine Art Benchmark dienen. Die historischen Daten können jedoch nicht kostenlos bezogen werden. In einigen Fällen können Sie einen kostenlosen Testzeitraum einplanen. Aber auch in diesem Fall müssen Sie sich auf der Börsen-Website registrieren und mit dem Vertriebsleiter kommunizieren. Auf der anderen Seite gibt es einen Wettbewerb zwischen den Brokern. Daher sollten sich die Kurse zwischen den Brokern nicht stark unterscheiden. In der Regel speichern Makler jedoch keine Tick-Archive und stellen sie nicht zum Download zur Verfügung.
Eine der kostenlosen Quellen ist die Website der Bank Dukascopy.
Nach einer einfachen Registrierung kann man historischen Daten herunterladen, einschließlich Ticks.
Zeilen in der Datei bestehen aus 5 Spalten:
- Zeit
- Briefkurs (Ask)
- Geldkurs (Bid)
- Erworbenes Volumen
- Verkauftes Volumen
Abb.1 Symbole zum Herunterladen auf der Registerkarte Daten der App vom Quant Data Manager
Die Methode CiCustomSymbol::LoadTicks() wird an das in der obigen App verwendete csv-Dateiformat angepasst.
3. Stresstests von Handelsstrategien
Das Testen der Handelsstrategie ist ein mehrstufiger Prozess. Meistens bezieht sich "Testen" auf die Ausführung eines Handelsalgorithmus mit historischen Kursen (Backtesting). Aber es gibt auch andere Methoden, um eine Handelsstrategie zu testen.
Eine davon ist der Stresstest.
Stresstests sind eine Form von bewusst intensiven oder gründlichen Tests, um die Stabilität eines bestimmten Systems oder einer bestimmten Einheit zu bestimmen.
Die Idee ist einfach: Schaffen Sie spezifische Bedingungen für den Betrieb der Handelsstrategie, die den Stress für die Strategie beschleunigen. Das ultimative Ziel solcher Bedingungen ist es, den Grad der Zuverlässigkeit des Handelssystems und seine Widerstandsfähigkeit gegen Bedingungen zu überprüfen, die sich verschlechtert haben oder verschlechtern können.
3.1 Ändern des Spread
Der Spread-Faktor ist für eine Handelsstrategie von großer Bedeutung, da er die Höhe der Zusatzkosten bestimmt. Die Strategien, die auf kurzfristige Geschäfte abzielen, sind besonders empfindlich gegenüber dem Spread. In einigen Fällen kann das Verhältnis von Spread zum Ergebnis 100% übersteigen.
Versuchen wir, ein nutzerdefiniertes Symbol zu erstellen, das sich vom Spread der Basis unterscheidet. Dazu erstellen wir eine neue Methode CiCustomSymbol::ChangeSpread().
//+------------------------------------------------------------------+ //| Change the initial spread | //| Input parameters: | //| 1) _spread_size - the new fixed value of the spread, pips. | //| If the value > 0 then the spread value is fixed. | //| 2) _spread_markup - a markup for the floating value of the | //| spread, pips. The value is added to the current spread if | //| _spread_size=0. | //| 3) _spread_base - a type of the price to which a markup is | //| added in case of the floating value. | //+------------------------------------------------------------------+ bool CiCustomSymbol::ChangeSpread(const uint _spread_size,const uint _spread_markup=0, const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID) { if(_spread_size==0) if(_spread_markup==0) { ::PrintFormat(__FUNCTION__+": neither the spread size nor the spread markup are set!", m_name,::GetLastError()); return false; } int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS); ::ZeroMemory(m_tick); //--- copy ticks int tick_idx=0; uint tick_cnt=0; ulong from=1; double curr_point=this.GetProperty(SYMBOL_POINT); int ticks_copied=0; MqlDateTime t1_time; TimeToStruct((int)(m_from_msc/1e3),t1_time); t1_time.hour=t1_time.min=t1_time.sec=0; datetime start_datetime,stop_datetime; start_datetime=::StructToTime(t1_time); stop_datetime=(int)(m_to_msc/1e3); do { MqlTick custom_symbol_ticks[]; ulong t1,t2; t1=(ulong)1e3*start_datetime; t2=(ulong)1e3*(start_datetime+PeriodSeconds(PERIOD_D1))-1; ::ResetLastError(); ticks_copied=::CopyTicksRange(m_name,custom_symbol_ticks,COPY_TICKS_INFO,t1,t2); if(ticks_copied<0) { ::PrintFormat(__FUNCTION__+": failed to copy ticks for a %s symbol! Error code: %d", m_name,::GetLastError()); return false; } //--- there are some ticks for the current day else if(ticks_copied>0) { for(int t_idx=0; t_idx<ticks_copied; t_idx++) { MqlTick curr_tick=custom_symbol_ticks[t_idx]; double curr_bid_pr=::NormalizeDouble(curr_tick.bid,symbol_digs); double curr_ask_pr=::NormalizeDouble(curr_tick.ask,symbol_digs); double curr_spread_pnt=0.; //--- if the spread is fixed if(_spread_size>0) { if(_spread_size>0) curr_spread_pnt=curr_point*_spread_size; } //--- if the spread is floating else { double spread_markup_pnt=0.; if(_spread_markup>0) spread_markup_pnt=curr_point*_spread_markup; curr_spread_pnt=curr_ask_pr-curr_bid_pr+spread_markup_pnt; } switch(_spread_base) { case SPREAD_BASE_BID: { curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs); break; } case SPREAD_BASE_ASK: { curr_bid_pr=::NormalizeDouble(curr_ask_pr-curr_spread_pnt,symbol_digs); break; } case SPREAD_BASE_AVERAGE: { double curr_avg_pr=::NormalizeDouble((curr_bid_pr+curr_ask_pr)/2.,symbol_digs); curr_bid_pr=::NormalizeDouble(curr_avg_pr-curr_spread_pnt/2.,symbol_digs); curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs); break; } } //--- new ticks curr_tick.bid=curr_bid_pr; curr_tick.ask=curr_ask_pr; //--- flags curr_tick.flags=0; if(m_tick.bid!=curr_tick.bid) curr_tick.flags|=TICK_FLAG_BID; if(m_tick.ask!=curr_tick.ask) curr_tick.flags|=TICK_FLAG_ASK; if(curr_tick.flags==0) curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK; custom_symbol_ticks[t_idx]=curr_tick; m_tick=curr_tick; } //--- replace ticks int ticks_replaced=0; for(int att=0; att<ATTEMTS; att++) { ticks_replaced=this.TicksReplace(custom_symbol_ticks); if(ticks_replaced==ticks_copied) break; ::Sleep(PAUSE); } if(ticks_replaced!=ticks_copied) { ::Print(__FUNCTION__+": failed to replace the refreshed ticks!"); return false; } tick_cnt+=ticks_replaced; } //--- next datetimes start_datetime=start_datetime+::PeriodSeconds(PERIOD_D1); } while(start_datetime<=stop_datetime && !::IsStopped()); ::PrintFormat("\nReplaced ticks number: %I32u",tick_cnt); //--- return true; } //+------------------------------------------------------------------+
Wie kann man den Spread für ein nutzerdefiniertes Symbol ändern?
Erstens kann ein fester Spread eingestellt werden. Dies kann durch die Angabe eines positiven Wertes im Parameter _spread_size erreicht werden. Beachten Sie, dass dieser Teil im Tester funktioniert, trotz der folgenden Regel:
Im Strategietester gilt der Spread immer als veränderlich. D.h. SymbolInfoInteger(Symbol, SYMBOL_SPREAD_FLOAT) gibt immer true zurück.
Zweitens kann dem verfügbaren Spread ein Aufschlag hinzugefügt werden. Dies kann durch die Definition des Parameters _spread_markup erreicht werden.
Außerdem ermöglicht die Methode die Angabe des Preises, der als Referenzwert des Spread dient. Dies geschieht mit Hilfe der Enumeration ENUM_SPREAD_BASE.
//+------------------------------------------------------------------+ //| Spread calculation base | //+------------------------------------------------------------------+ enum ENUM_SPREAD_BASE { SPREAD_BASE_BID=0, // bid price SPREAD_BASE_ASK=1, // ask price SPREAD_BASE_AVERAGE=2,// average price }; //+------------------------------------------------------------------+
Wenn wir Bid (SPREAD_BASE_BID) verwenden, dann wird Ask = Bid + berechneter Spread. Wenn wir Ask (SPREAD_BASE_ASK) verwenden, dann wird Bid = Ask - berechneter Spread. Wenn wir den Durchschnittspreis (SPREAD_BASE_AVERAGE) verwenden, dann wird Bid = Durchschnittspreis - berechneter Spread/2.
Die Methode CiCustomSymbol::ChangeSpread() ändert nicht den Wert einer bestimmten Symboleigenschaft, sondern ändert den Spread bei jedem Tick. Die aktualisierten Ticks werden in der Tick-Basis gespeichert.
Überprüfen Sie die Spread-Funktion mit Hilfe der Methode TestChangeSpread.mq5. Wenn das Skript einwandfrei läuft, wird das folgende Protokoll im Journal hinzugefügt:
2019.08.30 12:49:59.678 TestChangeSpread (EURUSD,M1) Replaced ticks number: 354400
Das bedeutet, dass die Tick-Größe für alle zuvor geladenen Ticks geändert wurde.
Die folgende Tabelle zeigt meine Strategie-Testergebnisse mit unterschiedlichen Spread-Werten unter Verwendung von EURUSD-Daten (Tabelle 1).
Wert | Spread 1 (12-17 Points) | Spread 2 (25 Points) | Spread 2 (50 Points) |
---|---|---|---|
Anzahl der Positionen |
172 | 156 | 145 |
Nettogewinn, $ |
4 018.27 | 3 877.58 | 3 574.1 |
Max. Kapital-Drawdown, % |
11.79 | 9.65 | 8.29 |
Gewinn je Position, $ |
23.36 | 24.86 | 24.65 |
Tabelle 1. Testergebnisse mit unterschiedlichen Spreads
Die Spalte "Spread 1" zeigt die Ergebnisse mit einem realen, veränderlichen Spread (12-17 Points mit fünfstelligen Kursen).
Mit einem höheren Spread war die Anzahl der Positionen geringer. Dies führte zu einer Verringerung des Drawdowns. Darüber hinaus hat sich in
diesem Fall die Rentabilität des Handels erhöht.
3.2 Ändern der Stopp- und Freeze-Level
Einige Strategien können von den Level für Stopps und Freeze abhängen. Viele Broker bieten einen Stopp-Level gleich dem Spread und ohne Freeze-Level an. Manchmal können Broker diese Werte jedoch erhöhen. Dies geschieht in der Regel in Zeiten erhöhter Volatilität oder geringer Liquidität. Diese Informationen können den Handelsregeln des Brokers entnommen werden.
Hier ist ein Beispiel für einen Auszug aus solchen Handelsregeln:
"Der Mindestabstand, in dem Pending-Orders oder T/P- und S/L-Aufträge erteilt werden können, entspricht dem Spread des Symbols. 10 Minuten vor der Veröffentlichung wichtiger makroökonomischer Statistiken oder Wirtschaftsnachrichten kann der Mindestabstand für S/L-Aufträge auf 10 Spreads erhöht werden. 30 Minuten vor Handelsschluss steigt dieses Niveau für S/L-Orders auf 25 Spreads."
Diese Level (SYMBOL_TRADE_STOPS_LEVEL und SYMBOL_TRADE_FREEZE_LEVEL) können mit der Methode CiCustomSymbol::SetProperty() konfiguriert werden.
Bitte beachten Sie, dass die Symboleigenschaften im Tester nicht dynamisch geändert werden können. In der aktuellen Version arbeitet der Tester mit vorkonfigurierten, nutzerdefinierten Symbolparametern. Die Entwickler der Plattform arbeitet hart daran, den Tester zu aktualisieren. Es ist möglich, dass diese Funktion in naher Zukunft erscheint.
3.3 Veränderte Margenanforderungen
Individuelle Margenanforderungen können auch für ein nutzerdefiniertes Symbol festgelegt werden. Die Werte für die folgenden Parameter können programmgesteuert eingestellt werden: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. So können wir die Margenhöhe für das Handelsinstrument definieren.
Es gibt auch Margin-Identifikatoren für verschiedene Arten von Positionen und Volumina (SYMBOL_MARGIN_LONG, SYMBOL_MARGIN_SHORT etc.). Sie können manuell eingestellt werden.
Leverage-Variationen ermöglichen es, die Handelsstrategie in Bezug auf Resistenz gegen Drawdowns zu testen und Stop-Outs zu vermeiden.
Schlussfolgerung
Dieser Artikel beleuchtet einige Aspekte des Stresstests der Handelsstrategie.
Nutzerdefinierte Symboleinstellungen ermöglichen die Konfiguration von Parametern für eigene Symbole. Jeder Algo-Händler kann einen bestimmten Satz von Parametern auswählen, die für eine bestimmte Handelsstrategie erforderlich sind. Eine der aufgedeckten interessanten Optionen betrifft die Markttiefe eines nutzerdefinierten Symbols.
Das Archiv enthält eine Datei mit Ticks, die als Teil der Beispiele verarbeitet wurden, sowie die Quelldateien des Skripts und die eigene Symbolklasse.
Ich möchte mich bei fxsaber und bei den Moderatoren Artyom Trishkin und Slava für interessante Diskussionen in "Custom symbols, Fehler, Bugs, Fragen & Vorschläge" (auf Russisch) bedanken.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/7166
- 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.