MQL5 酷宝书:利用自定义品种进行交易策略压力测试

14 十一月 2019, 15:03
Denis Kirichenko
1
1 155

概述

不久之前,在 MetaTrader 5 终端中加入了一项新功能:可以创建自定义品种。 如今,在某些情况下,算法交易员可以自己充当经纪商,而此刻他们不需要交易服务器,即可完全控制交易环境。 然而,这里的“某些情况”仅指测试模式。 当然,自定义品种的定单不能由经纪商执行。 尽管如此,此功能令算法交易者可以更精心地测试其交易策略。

在本文中,我们将为此种测试创造条件。 我们先从自定义品种类开始。


1. 自定义品种类 CiCustomSymbol

标准库提供了一个能简化访问品种属性的类。 它就是 CSymbolInfo 类。 实际上,该类执行一个中间函数:根据用户请求,该类与服务器进行通信,并按所请求属性的数值形式从服务器接收响应。

我们的目的就是为自定义品种创建一个相似的类。 甚或,此类的功能更加广泛,因为我们需要添加创建、删除和其他一些方法。 另一方面,我们不会用到与服务器连接的方法。 其中包括 Refresh()、IsSynchronized()、等等。

用于创建交易环境的这个类封装了用来操控自定义品种的标准功能

这个类的声明结构如下所示。

//+------------------------------------------------------------------+
//| 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;
  };
//+------------------------------------------------------------------+

我们一开始要完成的方法,是从已选品种中生成功能齐备的自定义品种。


1.1 CiCustomSymbol::Create() 方法

为了能够操控所有自定义品种属性,我们需要创建它,或确保它已被创建。 

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

该方法将返回一个数值作为数字代码:

  • -1 — 创建符号时出错;
  • 0  — 该品种早前已创建;
  • 1  — 该品种已在当前方法调用期间成功创建。

以下是有关 _batch_size 和 _is_selected 参数的几句说明。

第一个参数(_batch_size)设置批次的大小,它将用于加载即时报价。 即时报价将被分批加载:首先将数据读取到辅助数组; 数组填充完毕后,数据将被加载到自定义品种的即时报价数据库(即时报价历史记录)。 一方面,利用这种方法,您不必创建庞大的数组;另一方面,则无需过于频繁地刷新即时报价数据库。 辅助即时报价数组的默认大小为一百万

第二个参数(_is_selected)确定我们是将即时报价直接写到数据库中,或是首先将它们添加到“市场观察”窗口中。

举例来说,我们运行 TestCreation.mql5 脚本,该脚本会创建一个自定义品种。

调用此方法之后返回的代码将显示在日志中。

2019.08.11 12:34:08.055 TestCreation (EURUSD,M1) A custom symbol "EURUSD_1" creation has returned the code: 1

有关创建自定义品种的更详尽信息,请参阅文档


1.2 CiCustomSymbol::Delete() 方法

此方法将尝试删除自定义品种。 在删除品种之前,该方法将尝试从“市场观察”窗口中将其删除。 如果失败,删除过程将被中断。

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

例如,我们启动一个简单的脚本 TestDeletion.mql5,该脚本将删除自定义品种。 如果成功,则将相应记录输出到日志。

2019.08.11 19:13:59.276 TestDeletion (EURUSD,M1) A custom symbol "EURUSD_1" has been successfully deleted.



1.3 CiCustomSymbol::Clone() 方法

此方法执行克隆:基于所选品种,它检测当前自定义品种的属性。 简而言之,我们接收原始品种的属性值,并将其复制给另一个品种。 用户还可以设置即时报价历史记录的克隆。 为此,您需要定义时间间隔。

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+


请注意,并非所有属性都可以复制,因为其中一些属性仅可在终端级别设置。 可以检索它们的值,但不能对其进行控制(只读-属性)。

尝试为自定义品种的只读-属性设置数值将返回错误 5307(ERR_CUSTOM_SYMBOL_PROPERTY_WRONG)。 甚至于,对于自定义品种,在“运行时错误”部分之下还有一个单独的错误代码组

举例来说,我们运行一个简单的脚本 TestClone.mql5,该脚本将克隆一个基准品种。 如果克隆尝试成功,则以下记录将出现在日志中。

2019.08.11 19:21:06.402 TestClone (EURUSD,M1) A base symbol "EURUSD" has been successfully cloned.



1.4 CiCustomSymbol::LoadTicks() 方法

此方法从文件中读取即时报价,并加载它们以备将来使用。 请注意,该方法会删除此自定义品种以前已有的即时报价数据库。 

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

在此变体中,以下即时报价结构字段被填充

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
  };

为第一个即时报价标记设置了 TICK_FLAG_BID | TICK_FLAG_ASK 值。 之后的数值取决于出价(bid)或要价(ask)哪个发生了变化。 如果价格没有变化,则应将其作为第一个即时报价。

build 2085 开始,可以通过简单地加载即时报价历史记录来生成一根柱线的历史记录。 如果历史记录已加载,我们能够程序化请求柱线历史记录。

举例来说,我们运行简单的脚本 TestLoad.mql5,该脚本从文件中加载即时报价。 数据文件必须位于 %MQL5/Files 文件夹之下。 在此例中,文件是 EURUSD1_tick.csv。 它含有自 2019 年 8 月 1 日至 2 日的 EURUSD 即时报价。 往后,我们将研究即时报价数据源。

运行脚本后,被加载的即时报价数量将显示在日志中。 另外,我们应复查从终端的即时报价数据库中通过请求数据得来的可用即时报价的数量。 354,400 个即时报价已被 复制。 所以,数量相等。 我们还得到了 2,697 根一分钟柱线。 

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


其他方法属于 API 方法组。


2. 即时报价数据 来源

即时报价数据形成了一个价格序列,该序列具有非常活跃的生命力,供需关系彼此竞争。

价格系列的本质长期以来一直是交易员和专家们讨论的主题。 该序列作为分析的主题、决策的基础,等等。

以下思路与本文的上下文相适。

如您所知,外汇是场外交易市场。 所以,没有基准报价。 结果就是,没有可供参考的即时报价存档(即时报价历史记录)。 但是有货币期货,其中的大多数于芝加哥商品交易所进行交易。 这些报价可作为某种类型的基准。 但是,历史数据无法免费获得。 在某些情况下,您可以得到一段免费的测试期。 但即使在这种情况下,您也须在交易网站上注册,并与销售经理协商。 另一方面,经纪商之间也存在竞争。 因此,经纪商之间的报价差异不应该太大。 但是通常情况下,经纪商不会保留即时报价存档,也不会提供它们的下载。

免费来源之一是 Dukascopy Bank 网站

简单注册后,它允许下载历史数据,包括即时报价。

文件中的行由 5 列组成:

  • 时间
  • 要价
  • 出价
  • 采购量
  • 抛售量
手动下载即时报价的不便之处在于您只能选择一天。 如果您需要若干年的即时报价历史,则下载将花费大量的时间和精力。


Quant Data Manager

图例1 Quant Data Manager 应用程序的“数据”选项卡上可供下载的品种


有一些辅助应用程序可从 Dukascopy Bank 网站下载即时报价存档。 其中之一是 Quant Data Manager。 图例 1 展示了该应用程序窗口的选项卡之一。 


CiCustomSymbol::LoadTicks() 方法已调整为上述应用程序中使用的 csv 文件格式。


3. 交易策略之压力测试

交易策略测试是一个多层面的过程。 最常见的“测试”是指基于历史报价执行交易算法(回测)。 但是还有其他方法也可以测试交易策略。 

其中之一是压力测试

压力测试是一种特意为之的严格测试,或彻底测试,用于判断给定系统或实例的稳定性。

这一思路很简单:为交易策略的操作创建特定条件,这将为策略施加更劲爆的压力。 这种条件的终极目标是检验交易系统的可靠性程度,及其经受更恶略条件的承受力。


3.1 点差变化

点差因子对于交易策略极端重要,因为它决定了额外成本的数额。 瞄准短线交易的策略对点差大小尤为敏感。 在某些情况下,点差与回报之比可能超过 100%。 

我们尝试创建一个自定义品种,它与基准点差大小会有不同。 为此目的,我们创建一个新方法 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;
  }
//+------------------------------------------------------------------+

如何更改自定义品种的点差大小?

首先,可以设置固定点差。 这可通过在 _spread_size 参数中指定一个正数值来完成。 请注意,尽管有以下规则,此部分仍会在测试器中工作:

在策略测试器当中,点差始终认为是浮动的。 即 SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) 始终返回 true。

其次,可以将标记添加到可用点差值。 这可通过定义 _spread_markup 参数来完成。 

还有,该方法允许指定价格,该价格将用作参考点差值。 这是通过 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
  };
//+------------------------------------------------------------------+

如果我们采用出价(bid,SPREAD_BASE_BID),则要价(ask)= 出价(bid)+ 计算出的点差。 如果我们采用要价(ask,SPREAD_BASE_ASK),则出价(bid)= 要价(ask)- 计算出的点差。 如果我们采用平均价格(SPREAD_BASE_AVERAGE),则出价(bid)= 平均价格 - 计算出的点差/ 2。

CiCustomSymbol::ChangeSpread() 方法不会修改某个品种属性的值,但是会在每个即时报价来临时修改点差值。 更新后的即时报价存储在即时报价数据库中。


利用 TestChangeSpread.mq5 检查方法操作的点差。 如果脚本运行正常,则以下记录将添加到日志中:

2019.08.30 12:49:59.678 TestChangeSpread (EURUSD,M1)    Replaced ticks number: 354400


这意味着先前加载的全部即时报价的数量已有所变化。 


下表展示选用 EURUSD 数据(表 1),以不同点差值进行的策略测试结果。

点差 1 (12-17 点数) 点差 2 (25 点数) 点差 3 (50 点数)
交易数量
172 156 145
净盈利, $
4 018.27 3 877.58 3 574.1
最大 净值回撤, %
11.79 9.65 8.29
 每笔交易盈利, $
 23.36  24.86  24.65

表 1. 以不同点差值进行的测试结果

“点差 1” 列反映的是真实浮动点差的结果(12-17 点数,报价含五位小数)。

点差越高,交易数量就越少。 这导致回撤降低。 此外,在这种情况下,交易的获利能力有所提高。


3.2 修改停止和冻结价位

一些策略也许会依赖停止价位和冻结价位。 许多经纪商提供的停止价位等于点差和零冻结价位。 不过,有时经纪商能够增加这些价位。 通常,这会发生在波动性增加,或流动性较低的时期。 该信息可以从经纪商的交易规则中获取。 

此处是从这类交易规则里摘录的示例:

“挂单或止盈和止损订单的最小距离等于交易品种的点差。 重要的宏观经济统计数据或财经新闻发布前 10 分钟,止损订单的最小距离可以增加到 10 倍点差。 交易收市时间前 30 分钟,此止损订单价位提升到 25 倍点差。”

可以利用 CiCustomSymbol::SetProperty() 方法配置这些价位(SYMBOL_TRADE_STOPS_LEVEL 和 SYMBOL_TRADE_FREEZE_LEVEL)。 

请注意,不能在测试器中动态更改品种属性。 在当前版本中,测试器采用预配置的自定义品种参数进行操作。 平台开发人员介入明显。 所以,此功能可能会加入未来的版本之中。


3.3 修改保证金需求

还可以为自定义品种设置单独的保证金需求。 可以通过编程设置以下参数的值:SYMBOL_MARGIN_INITIAL,SYMBOL_MARGIN_MAINTENANCE,SYMBOL_MARGIN_HEDGED。 因此,我们可以定义交易产品的保证金水平。 

还有针对持仓和交易量的不同类型的保证金标识符(SYMBOL_MARGIN_LONG,SYMBOL_MARGIN_SHORT等)。 它们均被设置为手动模式。 

杠杆变化可以测试交易策略的回撤承受能力,以避免爆仓。


结束语

本文着重介绍了交易策略压力测试的某些层面。

自定义品种设置允许您配置自有品种的参数。 每位算法交易者都可以选择一组特定的参数,这是特定交易策略所必需的。 揭示出的有趣选项之一涉及自定义品种的市场深度。

存档包含即时报价文件,这些文件已作为示例的一部分进行处理,还包括脚本的源文件和自定义品种类。 

我要感谢 fxsaber,版主 Artyom TrishkinSlava,感谢他们在“自定义品种 错误,瑕疵,问题 & 简易” 一帖中进行的有趣讨论(俄语)。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/7166

附加的文件 |
EURUSD1_tick.zip (3087.31 KB)
Custom.zip (10.37 KB)
最近评论 | 前往讨论 (1)
Shuai Fu
Shuai Fu | 2 12月 2019 在 04:07
希望您能回复我:为什么我编辑文档的时候不能插图,用户链接、优酷视频、表格、代码,中间唯独少了一个插入图片的功能 ,,,为什么??
利用 curl 解析 HTML 利用 curl 解析 HTML

本文论述利用第三方控件的简易 HTML 代码解析库。 特别是,它涵盖了诸多访问数据的可能性,甚至有些用往常的 GET 和 POST 请求都无法检索。 我们将选择一个页面不太大的网站,并尝试从该网站获取感兴趣的数据。

轻松快捷开发 MetaTrader 程序的函数库 (第十六部分) : 品种集合事件 轻松快捷开发 MetaTrader 程序的函数库 (第十六部分) : 品种集合事件

在本文中,我们将为所有函数库的对象创建一个新的基类,在其所有衍生类中加入事件功能,并基于新的基类开发用来跟踪品种集合事件的类。 我们还将修改帐户和帐户事件类,以便开发新的基本对象功能。

轻松快捷开发 MetaTrader 程序的函数库(第十七部分):函数库对象之间的交互 轻松快捷开发 MetaTrader 程序的函数库(第十七部分):函数库对象之间的交互

在本文中,我们将完成所有函数库对象的基准对象开发,以便任何基于此函数库的对象都能够与用户进行交互。 例如,用户将能够设置开仓时可接受的点差大小,和预警价位,当点差达到该数值,或价格触及预警价位时,来自品种对象的事件将被一并发送到监听此信号的程序。

轻松快捷开发 MetaTrader 程序的函数库(第十八部分):帐户与任意其他函数库对象之间的交互 轻松快捷开发 MetaTrader 程序的函数库(第十八部分):帐户与任意其他函数库对象之间的交互

本文将帐户对象的操作安置于任意函数库对象的新基准对象之上,改进了 CBaseObj 基准对象,并测试了设置跟踪参数,以及接收任意函数库对象事件。