MQL5 酷宝书:利用自定义品种进行交易策略压力测试(基础篇)
📘

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

第 1/3 篇

◍ 用自定义品种给策略上压力

MT5 自 build 2000 往后支持 Custom Symbol,能在不接真实报价的情况下,把一段手工编造或历史变形的行情挂成独立品种。对做价格行为系统的人来说,这等于拥有了一座私有的行情实验室:你可以把 2015 年瑞郎黑天鹅那根跳空线复制十遍,专门看策略在极端缺口下的平仓逻辑会不会崩。 实际用途很直接——把实盘里难得一遇的极端样本,变成可反复跑的常规测试用例。比如 EURUSD 真实数据里很少连续 5 根实体超 200 点的 K 线,但自定义品种里你能一键生成,立刻验证止损间距和加仓规则是否扛得住。外汇与贵金属本身杠杆高、跳空频繁,这类压力测试不是锦上添花,而是上线前的一道硬关卡。 创建入口在 MT5 市场报价窗口右键「自定义品种」→「创建自定义品种」,之后用 CSV 导入或代码写入分时即可。下面这段是 MQL5 里用代码建一个自定义品种并塞入一根人为制造的大阴线的核心片段,跑通它你就能脱离手动界面,批量造行情。

「用自定义品种自己搭测试场」

MT5 近期加入了自定义品种功能,算法交易者能在无交易服务器的情况下,以测试模式完全掌控交易环境。这里的控制仅限回测,定单不会被任何经纪商执行,但足以让你脱离实盘约束验证策略逻辑。 外汇与贵金属属高风险品种,自定义品种回测不能替代实盘滑点与流动性检验,结论仅代表历史环境倾向。 本文先落地的,是自定义品种类的骨架,后续才能批量造出可控行情供 EA 跑。

自定义品种类怎么封装常用操作

标准库里的 CSymbolInfo 负责跟服务器要品种属性,相当于一层中间函数。做自定义品种时我们不必保留它的联网逻辑(Refresh、IsSynchronized 这类用不上),但要扩出创建、删除、克隆等终端级功能,于是有了 CiCustomSymbol 这个类,把自定义品种的常用操控收进一个封装里。 Create() 返回三种整数码:-1 是创建出错,0 表示品种此前已存在,1 代表本次调用才建成功。它有两个容易忽略的参数:_batch_size 控制即时报价分批写库的批次大小,辅助数组默认一百万条,既不用开巨大数组,也不至于频繁刷库;_is_selected 决定报价直接落库还是先塞进“市场观察”窗口。 Delete() 会先尝试把品种从“市场观察”移除,失败就中断删除。Clone() 则是把源品种属性抄给另一个自定义品种,可指定时间段克隆报价历史;但要注意只读属性(终端级设置)无法被改写,强行赋值会报 5307(ERR_CUSTOM_SYMBOL_PROPERTY_WRONG)。 LoadTicks() 从文件读报价并覆盖原库的 tick 历史。自 build 2085 起,加载完报价就能直接程序化要柱线。实测用 EURUSD1_tick.csv(2019.8.1–2 的 EURUSD tick)跑 TestLoad,日志显示复制了 354,400 条报价,与终端数据库请求数一致,并生成 2,697 根一分钟柱线。外汇与贵金属自定义品种测试属高风险操作,克隆或加载前务必确认数据源可靠。 打开 MT5 把 TestCreation / TestDeletion / TestClone / TestLoad 四个脚本依次跑一遍,看日志返回码和报价数量,就能验证这套类是否按预期工作。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CiCustomSymbol.                                                        |
class=class="str">"cmt">//| Purpose: Base class for a custom symbol.                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CiCustomSymbol : class="kw">public CObject
  {
   class=class="str">"cmt">//--- === Data members === ---
class="kw">private:
   class="type">class="kw">string             m_name;
   class="type">class="kw">string             m_path;
   class="type">MqlTick            m_tick;
   class="type">class="kw">ulong              m_from_msc;
   class="type">class="kw">ulong              m_to_msc;
   class="type">uint               m_batch_size;
   class="type">bool               m_is_selected;
   class=class="str">"cmt">//--- === Methods === ---
class="kw">public:
   class=class="str">"cmt">//--- constructor/destructor
   class="type">void               CiCustomSymbol(class="type">void);
   class="type">void              ~CiCustomSymbol(class="type">void) {};
   class=class="str">"cmt">//--- create/class="kw">delete
   class="type">int                Create(class="kw">const class="type">class="kw">string _name,class="kw">const class="type">class="kw">string _path="",class="kw">const class="type">class="kw">string _origin_name=NULL,
                              class="kw">const class="type">uint _batch_size=class="num">1e6,class="kw">const class="type">bool _is_selected=class="kw">false);
   class="type">bool               Delete(class="type">void);
   class=class="str">"cmt">//--- methods of access to class="kw">protected data
   class="type">class="kw">string             Name(class="type">void) class="kw">const { class="kw">return(m_name); }
   class="type">bool               RefreshRates(class="type">void);
   class=class="str">"cmt">//--- fast access methods to the integer symbol properties

◍ 自定义品种类的核心接口长这样

在 MT5 里做自定义品种( synthetic symbol )时,真正干活的是一组封装好的成员函数,而不是你去直接改底层数组。下面这段声明集中暴露了选择、克隆、点差与报价会话等能力,开 MT5 新建个 include 头文件就能对照着用。 Select(void) 返回当前品种是否被选中;Select(const bool select) 则主动切换选中态,写回测前先确认选中避免读空。Clone 从真实品种按毫秒区间拷贝分笔,默认起止为 0 表示全量,做黄金跨周期重采样时常把 _from_msc 设为 09:00 的毫秒偏移。 ChangeSpread 是调点差的关键:_spread_size 设点值点数,_spread_markup 加额外加点,_spread_base 默认 SPREAD_BASE_BID 表示点差挂在买价上。外汇与贵金属杠杆高,模拟时点差设错会让历史回测的滑点严重失真。 SetProperty / GetProperty 三套重载分别吃 double、long、string 枚举,对应价格精度、成交量位数、品种描述等字段。RatesReplace 与 RatesUpdate 接收 MqlRates 数组,前者替换区间返回受影响条数,后者只更新不增删;TicksAdd 吃 MqlTick 数组补分笔。 别在真实账户品种上直接调 RatesDelete,它按 datetime 区间物理删 K 线,误删会影响后续 EA 的 iClose 取值。

MQL5 / C++
  class="type">bool                Select(class="type">void) class="kw">const;
  class="type">bool                Select(class="kw">const class="type">bool select);
  class=class="str">"cmt">//--- service methods
  class="type">bool                Clone(class="kw">const class="type">class="kw">string _origin_symbol,class="kw">const class="type">class="kw">ulong _from_msc=class="num">0,class="kw">const class="type">class="kw">ulong _to_msc=class="num">0);
  class="type">bool                LoadTicks(class="kw">const class="type">class="kw">string _src_file_name);
  class="type">bool                ChangeSpread(class="kw">const class="type">uint _spread_size,class="kw">const class="type">uint _spread_markup=class="num">0,
                                   class="kw">const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID);
  class=class="str">"cmt">//--- API
  class="type">bool                SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,class="type">class="kw">double _val) class="kw">const;
  class="type">bool                SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,class="type">long _val) class="kw">const;
  class="type">bool                SetProperty(ENUM_SYMBOL_INFO_STRING _property,class="type">class="kw">string _val) class="kw">const;
  class="type">class="kw">double              GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) class="kw">const;
  class="type">long                GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) class="kw">const;
  class="type">class="kw">string              GetProperty(ENUM_SYMBOL_INFO_STRING _property) class="kw">const;
  class="type">bool                SetSessionQuote(class="kw">const ENUM_DAY_OF_WEEK _day_of_week,class="kw">const class="type">uint _session_index,
                                       class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to);
  class="type">bool                SetSessionTrade(class="kw">const ENUM_DAY_OF_WEEK _day_of_week,class="kw">const class="type">uint _session_index,
                                       class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to);
  class="type">int                 RatesDelete(class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to);
  class="type">int                 RatesReplace(class="kw">const class="type">class="kw">datetime _from,class="kw">const class="type">class="kw">datetime _to,class="kw">const class="type">MqlRates &_rates[]);
  class="type">int                 RatesUpdate(class="kw">const class="type">MqlRates &_rates[]) class="kw">const;
  class="type">int                 TicksAdd(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const;

「自定义品种创建函数的返回码与批次下限」

在 MT5 里封装自定义品种类时,Create 方法的返回码约定很直接:-1 表示创建失败,0 表示品种已存在无需重建,1 表示新建成功。调用方靠这三个整数就能决定后续是走克隆 tick 还是直接读已有数据。 _batch_size 参数不是摆设。代码里写了硬下限:若传入值小于 1e2(即 100),直接 Print 报错「a batch size must be greater than 100!」并放弃创建,返回 -1。实际默认值是 1e6,也就是百万级 tick 批量处理,小批量硬塞会被拦在门口。 别把正态当圣经:返回 0 不代表出错,只是告诉你「这品种我之前建过了」。写 EA 初始化时若不分清 0 和 -1,可能重复走克隆逻辑白白耗资源。开 MT5 随便建个自定义品种连跑两次 Create,第二次就会吃到 0 而不是 1。

MQL5 / C++
class="type">int TicksDelete(class="kw">const class="type">long _from_msc,class="type">long _to_msc) class="kw">const;
class="type">int TicksReplace(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const;
class=class="str">"cmt">//---
class="kw">private:
  class="kw">template<class="kw">typename PT>
  class="type">bool CloneProperty(class="kw">const class="type">class="kw">string _origin_symbol,class="kw">const PT _prop_type) class="kw">const;
  class="type">int CloneTicks(class="kw">const class="type">MqlTick &_ticks[]) class="kw">const;
  class="type">int CloneTicks(class="kw">const class="type">class="kw">string _origin_symbol) class="kw">const;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a custom symbol                                          |
class=class="str">"cmt">//| Codes:                                                          |
class=class="str">"cmt">//|    -class="num">1 - failed to create;                                       |
class=class="str">"cmt">//|     class="num">0 - a symbol exists, no need to create;                     |
class=class="str">"cmt">//|     class="num">1 - successfully created.                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CiCustomSymbol::Create(class="kw">const class="type">class="kw">string _name,class="kw">const class="type">class="kw">string _path="",class="kw">const class="type">class="kw">string _origin_name=NULL,
                           class="kw">const class="type">uint _batch_size=class="num">1e6,class="kw">const class="type">bool _is_selected=class="kw">false)
  {
   class="type">int res_code=-class="num">1;
   m_name=m_path=NULL;
   if(_batch_size<class="num">1e2)
     {
      ::Print(__FUNCTION__+": a batch size must be greater than class="num">100!");
     }
   else
     {
      ::ResetLastError();
      class=class="str">"cmt">//--- 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=class="num">0;
           }
         else

自定义品种建完之后怎么从市场报价里摘掉

上面那段创建逻辑里,res_code>=0 时才会把 m_name、m_path、m_batch_size 以及 m_is_selected 写进对象内部,并把是否加入市场报价交给 Select() 去处理。若要求加入但两次 Select 都失败,直接返回 false 并打出错误码,EURUSD_1 在日志里返回 code 1 就说明创建本身过了、但选入动作可能还需手动确认。 删除动作从 CiCustomSymbol::Delete() 开始,先 ResetLastError() 清掉旧错误码;若当前已选入市场报价,先尝试 Select(false) 摘牌,失败就报错返回 false。随后才调用 CustomSymbolDelete(m_name) 真正删品种,外盘贵金属与外汇品种用自定义符号回测完记得主动删,避免 MT5 品种树越堆越乱。 日志里 2019.08.11 12:34:08.055 的 TestCreation 行显示 EURUSD_1 创建返回 1,这是你上机验证时该复现的第一个数据点:开 MT5 跑同款类,看自己日志是否也落 1。

MQL5 / C++
::PrintFormat(__FUNCTION__+": failed to create a custom symbol. Error code: %d",::GetLastError());
   }
  }
  else
   res_code=class="num">1;
  if(res_code>=class="num">0)
  {
   m_name=_name;
   m_path=_path;
   m_batch_size=_batch_size;
   class=class="str">"cmt">//--- 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());
        class="kw">return class="kw">false;
       }
    }
   else
    {
     if(this.Select())
      if(!this.Select(class="kw">false))
       {
        ::PrintFormat(__FUNCTION__+": failed to unset the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
        class="kw">return class="kw">false;
       }
    }
   m_is_selected=_is_selected;
  }
 }
class=class="str">"cmt">//---
 class="kw">return res_code;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CiCustomSymbol::Delete(class="type">void)
  {
  ::ResetLastError();
  if(this.Select())
   if(!this.Select(class="kw">false))
    {
     ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError());
     class="kw">return class="kw">false;
    }
  if(!::CustomSymbolDelete(m_name))
   {

常见问题

用自定义品种功能手动或脚本生成一段夸张波动的K线数据,挂到市场报价里当独立品种跑回测,测完直接从报价窗口移除即可。
创建函数返回码要查专用状态码,批量建品种时单次下限建议不低于平台默认值,具体看接口文档的返回码表再调。
小布可对接你的自定义品种配置,自动按你设的极端参数跑诊断并汇总薄弱点,打开对应品种页就能看结果。
核心增删改查和报价挂载需自己封装成类方法,底层创建与移除函数平台已提供,直接调返回码判断成败。
在代码收尾处调用移除接口按名称摘掉,或写个清理脚本批量卸载,避免残留占用终端资源。