MetaTrader 5中的蒙特卡罗置换测试(基础篇)
📘

MetaTrader 5中的蒙特卡罗置换测试(基础篇)

第 1/3 篇

在MT5里跑蒙特卡罗置换测试

蒙特卡罗置换测试的核心,是把历史交易序列的顺序随机打乱后重算权益曲线,用来检验策略收益到底靠的是时序逻辑还是单纯运气。MT5 本身不内置这套流程,需要借助 MQL5 脚本把已平仓订单按随机排列重放。 一个直观的判据:若原始权益曲线落在 1000 次随机置换结果的 95% 分位以外,说明该策略的时序结构可能真有边际优势;若大量落在中位附近,则大概率只是样本内噪声。外汇与贵金属杠杆高,这类统计结论只代表历史样本特征,实盘仍属高风险。 下面这段脚本演示了如何读取历史成交并按随机种子重排后输出权益路径,开 MT5 后可直接挂到脚本区验证。

「把价格序列打散再做EA测试」

Aleksey Nikolayev 曾提出一种置换思路:把测试里的交易顺序随机重排来做显著性检验,他也顺带提过另一种玩法——直接打乱价格数据的先后顺序,再把同一个 EA 跑在成百上千个重排后的价格序列上,看原样本里的绩效是不是只是随机巧合。 他在文章里倾向认为,MetaTrader 5 没法对任意 EA 直接做这类价格置换测试,至少不完全可行。这个结论下得有点早。 本文就用 MT5 实打实演示一遍:如何对价格序列做随机排列,并给出可直接用的置换代码,以及一个能自动跑完前置步骤的脚本,让你在正式对完整 EA 做置换检验前少写一堆胶水代码。外汇与贵金属品种波动剧烈、跳空频繁,重排后的价格只是统计参照,不代表真实行情,实盘前务必清醒认识高风险。

◍ 用乱序重排逼出EA的真实能力

置换测试的核心思路是:先拿一段真实价格样本跑EA,记下你关心的绩效指标,比如胜率或净值曲线斜率。随后把这段价格序列的顺序随机打乱,再跑同一套EA并记录同样的指标。 每次重排都是一次独立试验,原顺序被彻底破坏,趋势与周期结构不再存在。若EA在乱序数据上表现和真实数据接近,说明它很可能只是撞上了样本里的随机波动。 实操上重排次数至少100次,理想是数千次;次数越多,统计置信越稳健,但MT5单线程跑大规模重排会明显吃CPU。外汇与贵金属属高杠杆高风险品种,任何统计结论只代表历史样本概率,不构成方向承诺。 我们真正想从这套流程里逼问的是:EA在真实价格里赚到的钱,到底来自可重复的逻辑,还是来自这段特定序列的偶然排列。

用 p 值戳穿 EA 的运气成分

跑完大量迭代后,你会攒下一堆排列的性能数据,夏普、利润因子或纯净利润都行。假设做了 99 次置换加 1 次原始测试,共 100 个样本可供横比。 下一步数清楚原始测试被多少置换结果反超,把这个数当分数。在假设检验里它就是 p 值:EA 本没盈利潜力却瞎蒙出同样或更好结果的概率。 接着上面的 100 次迭代,若有 29 个置换比基准好,p 值就是 (29+1)/(100+1)=0.3。说明亏钱 EA 撞大运达到类似表现的概率有三成。看着还行,但我们要的 p 值尽量贴近零,0.05 及以下才拿得出手。 完整公式: (z+1)/(r+1),r 是置换总数,z 是性能优于基准的置换数。置换过程本身得严谨,否则 p 值毫无意义。外汇与贵金属测试高危,样本偏差会放大实盘回撤。

「置换价格序列前先搞定随机数与对数转换」

做价格序列重排,第一道关是随机数必须服从 0 到 1 的均匀分布,否则任何排列组合的概率不等,后续检验全废。MT5 统计库里直接有现成工具可用,能指定上下界,不用自己造轮子。 金融时间序列不能动实际价格的位置,那样时间结构就碎了。正确做法是先对价格做对数转换,再调整价格变化量,把原始价差的影响压到最低。第一个价格点必须原样保留、踢出排列池,重构后首尾价不变,只打乱中间波动——原趋势因此被保留。 用分时数据比搬柱形图轻松得多,但分时里混着成交量、时间、分时标志。时间和标志不动,只动报价、询价和成交量;麻烦在于这三者都可能为 0,直接套对数会爆掉,代码里得先排雷。 下面这段是均匀分布随机数函数,入参 a 为下界、b 为上界,带错误码引用。先拦 NaN 和 b<a 的非法区间,返回安静 NaN;a==b 直接退 a;正常路径用 MathRandomNonZero 乘区间宽加偏移。开 MT5 把这段塞进脚本,跑一万次看直方图是否平,就能验证随机源靠不靠谱。外汇与贵金属波动剧烈,这类重排仅用于假设检验,实盘直接套用风险极高。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Random variate from the Uniform distribution                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Computes the random variable from the Uniform distribution          |
class=class="str">"cmt">//| with parameters a and b.                                           |
class=class="str">"cmt">//|                                                                    |
class=class="str">"cmt">//| Arguments:                                                         |
class=class="str">"cmt">//| a          : Lower endpoint(minimum)                              |
class=class="str">"cmt">//| b          : Upper endpoint(maximum)                              |
class=class="str">"cmt">//| error_code : Variable for error code                               |
class=class="str">"cmt">//|                                                                    |
class=class="str">"cmt">//| Return value:                                                      |
class=class="str">"cmt">//| The random value with uniform distribution.                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double MathRandomUniform(class="kw">const class="type">class="kw">double a,class="kw">const class="type">class="kw">double b,class="type">int &error_code)
  {
class=class="str">"cmt">//--- check NaN
   if(!MathIsValidNumber(a) || !MathIsValidNumber(b))
     {
      error_code=ERR_ARGUMENTS_NAN;
      class="kw">return QNaN;
     }
class=class="str">"cmt">//--- check upper bound
   if(b<a)
     {
      error_code=ERR_ARGUMENTS_INVALID;
      class="kw">return QNaN;
     }
   error_code=ERR_OK;
class=class="str">"cmt">//--- check ranges
   if(a==b)
      class="kw">return a;
class=class="str">"cmt">//---
   class="kw">return a+MathRandomNonZero()*(b-a);
  }

◍ 分时置换类的内部运转

CPermuteTicks 把分时重排这件事拆成了三步预处理:原始分时进 m_ticks,对数变换后进 m_logticks,再做一阶差分收进 m_differenced。除了 ask、bid、成交量与真实成交量这四个数值成员,时间戳和分时标志一律不动,保证置换后的序列还能映射回真实行情节奏。 随机数区间由 MIN_THRESHOLD(1e-5) 与 MAX_THRESHOLD(1.0) 两个宏锁死。改这两个值要格外小心,必须让下限严格小于上限,否则 Uniform.mqh 抽出来的均匀分布会直接越界,预处理阶段就可能返回 false,m_initialized 立不起来,后面 Permute() 跑了也白跑。 调用顺序卡得很死:先实例化了对象,必须调 Initialize() 灌入待处理的分时数组;里面会按规模重设私有数组尺寸,并用 LogTransformTicks() 做对数变换,遇到零或负值统一替换成 1.0 避免对数域崩掉。置换完再靠 ExpTransformTicks() 私法反变换回原始域。 Permute() 只接一个动态 MqlTick 数组做输出参数。重排发生在 while 循环里,每轮用随机下标交换 m_differenced 的差分位置,跑完所有迭代后先撤差分重建 m_logticks,再把时间和标志从原序列抄回去。拿到这组输出,你就能在 MT5 里做价格序列的随机化对照测试——但这只是验证的一半,回测本身还得另写。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  PermuteTicks.mqh |
class=class="str">"cmt">//|                                     Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include<Math\Stat\Uniform.mqh>
class=class="str">"cmt">//+-----------------------------------------------------------------------------------+
class=class="str">"cmt">//| defines: representing range of random values from random number generator          |
class=class="str">"cmt">//+-----------------------------------------------------------------------------------+
class="macro">#define MIN_THRESHOLD class="num">1e-5
class="macro">#define MAX_THRESHOLD class="num">1.0
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="kw">struct to handle tick data to be worked on                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct CMqlTick
  {
   class="type">class="kw">double            ask_d;
   class="type">class="kw">double            bid_d;
   class="type">class="kw">double            vol_d;
   class="type">class="kw">double            volreal_d;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class to enable permutation of a collection of ticks in an array |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPermuteTicks
  {
class="kw">private :
   class="type">MqlTick           m_ticks[];          class=class="str">"cmt">//original tick data to be shuffled
   CMqlTick          m_logticks[];      class=class="str">"cmt">//log transformed tick data of original ticks
   CMqlTick          m_differenced[];   class=class="str">"cmt">//log difference of tick data
   class="type">bool              m_initialized;     class=class="str">"cmt">//flag representing proper preparation of a dataset
   class=class="str">"cmt">//helper methods
   class="type">bool              LogTransformTicks(class="type">void);
   class="type">bool              ExpTransformTicks(class="type">MqlTick &out_ticks[]);
class="kw">public :
   class=class="str">"cmt">//constructor
                     CPermuteTicks(class="type">void);
   class=class="str">"cmt">//desctrucotr

常见问题

用置换测试把收盘价序列随机重排成多组新序列,分别跑EA回测,对比原序列收益分布即可判断。
对数转换能让价格波动更接近正态,避免重排后畸变;随机数种子固定才能保证每次置换可复现。
小布可内置AIGC诊断,直接对品种页调出置换重排测试,自动输出p值与收益分布,省去手敲脚本。
p值低于0.05仅表示原EA收益在乱序分布中属于小概率,真实能力倾向可信,但外汇贵金属仍属高风险。
分时置换只重排局部时间段顺序,更狠地破坏结构,比全序列乱序更易暴露EA对特定走势的记忆。