MQL5 酷宝书:利用自定义品种进行交易策略压力测试
概述
不久之前,在 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 列组成:
- 时间
- 要价
- 出价
- 采购量
- 抛售量
图例1 Quant Data Manager 应用程序的“数据”选项卡上可供下载的品种
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 Trishkin 和 Slava,感谢他们在“自定义品种 错误,瑕疵,问题 & 简易” 一帖中进行的有趣讨论(俄语)。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/7166