MetaTrader 5中的蒙特卡罗置换测试·综合运用
🎲

MetaTrader 5中的蒙特卡罗置换测试·综合运用

(3/3)· 前两步铺垫了概念与单EA改造,这里把随机置换价格、自动脚本与实跑示例拼成完整闭环

进阶 第 3/3 篇
很多交易者以为MT5只能对交易顺序做置换,没法对任意EA重排价格序列。其实标准库统计工具配合几段脚本就能做到,只是初始步骤手工做太磨人。本篇把这条路走通,让你直接拿到可复用的测试框架。

「用基类 tick 批量造自定义品种的类骨架」

想在 MT5 里做价格行为回测,又嫌真实历史太少?可以写一个类,把某个基类品种(比如 XAUUSD)的 tick 数据抽出来做排列,生成一批自定义符号。下面这个类 CGenerateSymbols 就是干这事的基础结构。 类的私有成员里,m_basesymbol 存基类名,m_symbols_id 给新符号加统一前缀,m_tickrangestart 和 m_tickrangestop 用 long 型毫秒时间戳框定抽取区间,m_permutations 记录要生成多少组排列。m_baseticks 装原始 tick,m_permutedticks 装洗牌后的 tick,m_csymbols 是指向已建符号对象的指针数组,m_shuffler 负责具体打乱。 Initiate 方法是入口:先清掉上次的排列计数,SymbolSelect 把基类加进观察列表,否则直接返回 false 并打印错误。日期区间若 start>=stop 会被拒,合法时乘 1000 转成毫秒存进成员变量。最后检查 m_shuffler 指针,无效就报错——这部分截断在原文里,但逻辑很直接:对象没建起来后面没法洗数据。 开 MT5 自己验证时,注意自定义品种和真实账户交易无关,外汇贵金属杠杆高、自定义数据不代表未来概率,仅作策略压力测试用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|CGenerateSymbols class                                                        |
class=class="str">"cmt">//| creates custom symbols from an existing base symbol&class="macro">#x27;s tick data              |
class=class="str">"cmt">//|  symbols represent permutations of base symbol&class="macro">#x27;s ticks                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CGenerateSymbols
{
class="kw">private:
   class="type">class="kw">string            m_basesymbol;     class=class="str">"cmt">//base symbol
   class="type">class="kw">string            m_symbols_id;     class=class="str">"cmt">//common identifier added to names of new symbols 
   class="type">long              m_tickrangestart; class=class="str">"cmt">//beginning date for range of base symbol&class="macro">#x27;s ticks
   class="type">long              m_tickrangestop;  class=class="str">"cmt">//ending date for range of base symbol&class="macro">#x27;s ticks
   class="type">uint              m_permutations;   class=class="str">"cmt">//number of permutations and ultimately the number of new symbols to create
   class="type">MqlTick           m_baseticks[];    class=class="str">"cmt">//base symbol&class="macro">#x27;s ticks
   class="type">MqlTick           m_permutedticks[];class=class="str">"cmt">//permuted ticks;
   CNewSymbol       *m_csymbols[];     class=class="str">"cmt">//array of created symbols
   CPermuteTicks *m_shuffler;          class=class="str">"cmt">//object used to shuffle tick data
   
class="kw">public:
   CGenerateSymbols(class="type">void);
   ~CGenerateSymbols(class="type">void);
   class="type">bool Initiate(class="kw">const class="type">class="kw">string base_symbol,class="kw">const class="type">class="kw">string symbols_id,class="kw">const class="type">class="kw">datetime start_date,class="kw">const class="type">class="kw">datetime stop_date);
   class="type">uint Generate(class="kw">const class="type">uint permutations);
};
class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+
class=class="str">"cmt">//|set and check parameters for symbol creation, download ticks and initialize tick shuffler|
class=class="str">"cmt">//+-----------------------------------------------------------------------------------------+
class="type">bool CGenerateSymbols::Initiate(class="kw">const class="type">class="kw">string base_symbol,class="kw">const class="type">class="kw">string symbols_id,class="kw">const class="type">class="kw">datetime start_date,class="kw">const class="type">class="kw">datetime stop_date)
{
class=class="str">"cmt">//---reset number of permutations previously done
m_permutations=class="num">0;
class=class="str">"cmt">//---set base symbol
m_basesymbol=base_symbol;
class=class="str">"cmt">//---make sure base symbol is selected, ie, visible in WatchList 
if(!SymbolSelect(m_basesymbol,true))
  {
   Print("Failed to select ", m_basesymbol," error ", GetLastError());
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---set symbols id 
m_symbols_id=symbols_id;
class=class="str">"cmt">//---check, set ticks date range
if(start_date>=stop_date)
  {
   Print("Invalid date range ");
   class="kw">return class="kw">false;
  }
else
  {
   m_tickrangestart=class="type">long(start_date)*class="num">1000;
   m_tickrangestop=class="type">long(stop_date)*class="num">1000;
  }  
class=class="str">"cmt">//---check shuffler object
   if(CheckPointer(m_shuffler)==POINTER_INVALID)
   {
    Print("CPermuteTicks object creation failed");

◍ Tick抓取与排列符号的生成循环

下载基准品种的tick是后续所有排列重采样的前提。代码里用 CopyTicksRange 按时间区间拉取 COPY_TICKS_ALL 全量tick,若单次返回小于等于0就 Sleep(500) 后重试,最多跑 MAX_DOWNLOAD_ATTEMPTS 次;连续失败会 Print 错误码并直接返回 false,实盘里若经纪商限制历史tick深度,这里大概率卡在 attempts 耗尽。 Generate() 负责批量造排列符号。入参 permutations 为0会被拒,数组按 m_permutations+permutations 用 ArrayResize 预留空间;循环从已有排列数起,逐个 new CNewSymbol 并拼出形如 BASE_<id>_<序号> 的名称,Create 返回大于0才继续。 克隆基准品种后调用 m_shuffler.Permute 打乱tick,再 TicksAdd 写回新符号。外汇与贵金属tick重采样属于高概率偏离真实市场的合成数据,仅适合做统计压力测试,不能直接当行情信号。

MQL5 / C++
   class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---download ticks
   Comment("Downloading ticks");
   class="type">uint attempts=class="num">0;
   class="type">int downloaded=-class="num">1;
   class="kw">while(attempts<MAX_DOWNLOAD_ATTEMPTS)
     {
      downloaded=CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,m_tickrangestart,m_tickrangestop);
      if(downloaded<=class="num">0)
        {
         Sleep(class="num">500);
         ++attempts;
        }
      else
        class="kw">break;  
     }
class=class="str">"cmt">//---check download result
   if(downloaded<=class="num">0)
    {
     Print("Failed to get tick data for ",m_basesymbol," error ", GetLastError());
     class="kw">return class="kw">false;
    }       
   Comment("Ticks downloaded");  
class=class="str">"cmt">//---class="kw">return shuffler initialization result   
   class="kw">return m_shuffler.Initialize(m_baseticks);      
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| generate symbols class="kw">return newly created or refreshed symbols        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CGenerateSymbols::Generate(class="kw">const class="type">uint permutations)
{
class=class="str">"cmt">//---check permutations
if(!permutations)
   {
    Print("Invalid parameter value for Permutations ");
    class="kw">return class="num">0;
   }
class=class="str">"cmt">//---resize m_csymbols
   if(m_csymbols.Size()!=m_permutations+permutations)
    ArrayResize(m_csymbols,m_permutations+permutations,RESIZE_RESERVE);
class=class="str">"cmt">//---
   class="type">class="kw">string symspath=m_basesymbol+m_symbols_id+"_PermutedTicks";
   class="type">int exists;
class=class="str">"cmt">//---do more permutations
   for(class="type">uint i=m_permutations; i<m_csymbols.Size(); i++)
     {
      if(CheckPointer(m_csymbols[i])==POINTER_INVALID)
          m_csymbols[i]=new CNewSymbol();
      exists=m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+
class="type">class="kw">string(i+class="num">1),symspath,m_basesymbol);
      if(exists>class="num">0)
        {
         Comment("new symbol created "+m_basesymbol+m_symbols_id+"_"+
class="type">class="kw">string(i+class="num">1) );
         if(!m_csymbols[i].Clone(m_basesymbol) || !m_shuffler.Permute(m_permutedticks))
           class="kw">break;
         else
           {
            m_csymbols[i].Select(true);
            Comment("adding permuted ticks");
            if(m_csymbols[i].TicksAdd(m_permutedticks)>class="num">0)

用置换重排给已有品种注入扰动 tick

这段逻辑处在品种遍历的 else 分支里:当目标符号已经存在于市场观察列表时,不再新建,而是直接对它动手。先通过 Select(true) 把该品种置为当前活跃符号,后续 tick 操作才有作用对象。 核心动作是调用 m_shuffler.Permute(m_permutedticks) 对原 tick 序列做置换,若返回失败立即 break 退出循环,避免脏数据写入。置换成功后用 TicksReplace 把重排后的 tick 写回品种,返回值大于 0 才把 m_permutations 计数器加一,否则同样 break。 从计数语义看,m_permutations 只统计「真正成功替换 tick 的品种数」,不是遍历次数。你在 MT5 里接这段时,可先打印 m_permutations 与 i 的终值,若两者差很多,说明中途有品种替换失败——外汇与贵金属品种在高波动时可能拒绝 tick 写入,属正常高风险表现,需查日志而非盲目重试。

MQL5 / C++
            m_permutations++;
            }
        }
    else
        {
         Comment("symbol exists "+m_basesymbol+m_symbols_id+"_"+"class="type">class="kw">string"(i+class="num">1) );
         m_csymbols[i].Select(true);
         if(!m_shuffler.Permute(m_permutedticks))
            class="kw">break;
         Comment("replacing ticks ");
         if(m_csymbols[i].TicksReplace(m_permutedticks)>class="num">0)
            m_permutations++;
         else
            class="kw">break;   
        }
    }
class=class="str">"cmt">//---class="kw">return successful number of permutated symbols
Comment("");
class=class="str">"cmt">//--- 
class="kw">return m_permutations;
}

「用置换品种跑出 p 值的水位线」

拿 MT5 自带的 MACD 示例 EA 做载体,在 AUDUSD 上先跑脚本生成 100 个置换分时品种,再进策略测试器做优化。测试器结果页会按你选的绩效标准降序排这些品种,右上角下拉菜单切标准即可。 原始 AUDUSD 的测试数字落在结果列表靠下位置,有 10 个以上置换品种表现更好,这直接说明 p 值高于 0.05,不用手算也能读出。 但这一轮测试周期太短,结论只能当方法演示,外汇与贵金属杠杆高、价格跳变频繁,真用于实盘前必须把测试期拉长到覆盖多种行情。 想精确算 p 值,右键从测试器导出 xml,用 OpenOffice Calc 打开后删掉无关品种行,在表底加一行写宏:参考置换品种性能(图中第 18 行)和每列对应值,几次点击就能出数。 会 python 或 mql5 的,也可以自己解析 xml,导出时挑个好找的目录就行。

◍ 把这条线请下神坛

置换测试的价值已经说透:它不挑 EA,不需要源码,靠的是不假设分布的稳健统计,能在你掏钱买策略前先戳穿伪优势。但代价也实在——跑一套下来既吃 CPU 也吃硬盘,生成置换品种和分时数据会快速占满空间,普通笔记本可能跑不动完整轮次。 附带的四个文件是落地这套思路的钥匙:GenerateSymbols.mqh 定义置换品种生成类,NewSymbol.mqh 管自定义品种创建,PermuteTicks.mqh 做分时数组洗牌,PrepareSymbolsForPermutationTests.mq5 一键备好测试环境。开 MT5 把 ZIP 里的 9.91 KB 包解进去,先拿自己怀疑的 EA 跑一遍置换,p 值若掉不到 0.05 以下,策略优势倾向只是样本巧合。 下面这段是对分时做对数变换的辅助方法,避免零值和负值捣乱: //+------------------------------------------------------------------+ //|应用对数变换的辅助方法 //+------------------------------------------------------------------+ bool CPermuteTicks::LogTransformTicks(void) { if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); for(uint i=0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>0)?MathLog(m_ticks[i].bid):MathLog(1e0); m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1e0); m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1e0); } return true; } 逐行看:先按原 tick 数组长度重整日志数组;循环里对 bid、ask、volume、volume_real 四个字段做条件对数变换,大于 0 才取真值对数,否则落到一个 1e0 的安全底上,防止 MathLog 算崩。外汇与贵金属杠杆高、滑点凶,置换结论只减误判概率,不替你挡实盘风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|应用对数变换的辅助方法
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPermuteTicks::LogTransformTicks(class="type">void)
  {
   if(m_logticks.Size()!=m_ticks.Size())
      ArrayResize(m_logticks,m_ticks.Size());
   for(class="type">uint i=class="num">0; i<m_ticks.Size(); i++)
     {
      m_logticks[i].bid_d=(m_ticks[i].bid>class="num">0)?MathLog(m_ticks[i].bid):MathLog(class="num">1e0);
      m_logticks[i].ask_d=(m_ticks[i].ask>class="num">0)?MathLog(m_ticks[i].ask):MathLog(class="num">1e0);
      m_logticks[i].vol_d=(m_ticks[i].volume>class="num">0)?MathLog(m_ticks[i].volume):MathLog(class="num">1e0);
      m_logticks[i].volreal_d=(m_ticks[i].volume_real>class="num">0)?MathLog(m_ticks[i].volume_real):MathLog(class="num">1e0);
     }
   class="kw">return true;
  }
把初始置换步骤交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到随机序列生成与EA批量调度雏形,把重复劳动交给小布,你专注决策阈值怎么定。

常见问题

公式是(z+1)/(r+1),r是置换次数,z是性能优于基准的置换数。样本外数据加均匀随机重排,是前提条件。
为保证每个可能序列等概率出现,否则p值偏差。MQL5统计库有现成均匀生成器可用。
次数越少置信越弱,千次级更稳;外汇贵金属高杠杆高风险,序列相关性本就脆弱,建议至少上千次再看p值。
目前小布内置的是品种页诊断与序列生成雏形,完整EA批量置换需借本篇脚本自行扩展,AIGC部分可省去手工准备价序。
只能给出无盈利潜力的偶然概率,p值近零倾向策略有边缘,但不等于稳赚,实盘仍受滑点与点差挤压。