Das MQL5-Kochbuch: Stresstests von Handelsstrategien unter Verwendung nutzerdefinierter Symbole

Denis Kirichenko | 1 November, 2019

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:

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:

Der Nachteil des manuellen Herunterladens von Ticks besteht darin, dass Sie nur einen Tag auswählen können. Wenn Sie eine mehrjährige Historie der Ticks benötigen, wird das Herunterladen viel Zeit und Mühe in Anspruch nehmen.


Quant Data Manager

Abb.1 Symbole zum Herunterladen auf der Registerkarte Daten der App vom Quant Data Manager


Es gibt Hilfsanwendungen, die Archive von Ticks von der Dukascopy Bank Website herunterladen. Einer von ihnen ist Quant Data Manager. Abbildung 1 zeigt eine der Registerkarten des Anwendungsfensters. 


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.