蒙特卡洛方法在交易策略优化中的应用·进阶篇
📘

蒙特卡洛方法在交易策略优化中的应用·进阶篇

第 2/2 篇

用均匀随机做数组重采样

下面这段 MQL5 函数演示了如何从一个双精度数组 a[] 里按均匀分布抽取下标,把结果写进 b[],实现一次简单的重采样。 重采样逻辑本身不复杂:先取数组长度 na,再对每一个目标位置 i 用 MathRandomUniform(0,na,ner) 生成一个 [0,na) 的浮点随机数,强转成整数 nc 后越界则回退到 na-1,最后 b[i]=a[nc]。 值得在 MT5 里实测的一个细节是:MathRandomUniform 返回的值必须经过 MathIsValidNumber 校验,否则一旦出现非有限数(NaN/Inf),后续下标强转会直接出错。代码里选择打印错误并调用 ExpertRemove() 终止脚本,这在实盘 EA 里偏激进,回测时可先改成 continue 观察抽样分布。 外汇与贵金属市场高杠杆、随机性极强,这类重采样仅用于样本扰动或集成测试,不代表任何方向概率优势,实盘使用须自行验证。

MQL5 / C++
  class="kw">return true;
  }
class="type">void sample(class="type">class="kw">double &a[],class="type">class="kw">double &b[])
  {
   class="type">int ner;
   class="type">class="kw">double dnc;
   class="type">int na=ArraySize(a);
   for(class="type">int i=class="num">0; i<na;++i)
     {
       dnc=MathRandomUniform(class="num">0,na,ner);
       if(!MathIsValidNumber(dnc)) {Print("MathIsValidNumber(dnc) error ",ner); ExpertRemove();}
       class="type">int nc=(class="type">int)dnc;
       if(nc==na) nc=na-class="num">1;
       b[i]=a[nc];
     }
  }

「用中位数扛住利润曲线的尖峰」

评估EA优化结果时,光看总利润容易被偶发大单带偏。把最终收益序列当成样本做统计,比值类指标比绝对数更有参考价值:分子是平均收益,分母是离散程度,值越大代表「赚得多且稳」。 两条路线都基于这个思路。mean_sd() 用算术平均除以标准差,med_intq() 用中位数除以四分位距。后者对极端值不敏感——某次散射造成的资金曲线尖峰,不会像第一种那样剧烈拉低评分。 实测里,同一组参数下 mean_sd 优选出的版本比纯最大利润优选的资金曲线更平滑;若想要末尾绝对收益更高,靠加手数就能补,不需要牺牲曲线质量。外汇与贵金属杠杆高,这种平滑度差异直接关系到回撤可控性,倾向用第二变体做初筛。 下面两段是 MT5 里可直接塞进优化模块的原式,注意 NSAMPLES 需先在全局定义采样次数:

MQL5 / C++
class="type">class="kw">double mean_sd(class="type">class="kw">double &k[])
  {
   class="type">class="kw">double km[],cn[NSAMPLES];
   class="type">int nk=ArraySize(k);
   ArrayResize(km,nk);
   for(class="type">int n=class="num">0;n<NSAMPLES;++n)
     {
      sample(k,km);
      cn[n]=class="num">1.0;
      for(class="type">int i=class="num">0;i<nk;++i) cn[n]*=km[i];
      cn[n]-=class="num">1.0;
     }
   class="kw">return MathMean(cn)/MathStandardDeviation(cn);
  }
class="type">class="kw">double med_intq(class="type">class="kw">double &k[])
  {
   class="type">class="kw">double km[],cn[NSAMPLES];
   class="type">int nk=ArraySize(k);
   ArrayResize(km,nk);
   for(class="type">int n=class="num">0;n<NSAMPLES;++n)
     {
      sample(k,km);
      cn[n]=class="num">1.0;
      for(class="type">int i=class="num">0;i<nk;++i) cn[n]*=km[i];
      cn[n]-=class="num">1.0;
     }
   ArraySort(cn);
   class="kw">return cn[(class="type">int)(class="num">0.5*NSAMPLES)]/(cn[(class="type">int)(class="num">0.75*NSAMPLES)]-cn[(class="type">int)(class="num">0.25*NSAMPLES)]);
  }

◍ 用剩余资金比例掐断扛单回路

在批量模拟交易序列时,可以实时盯住账户剩余资金占初始或峰值的比例。一旦跌破 rmndmin 设定的阈值,后续交易直接作废,资金曲线尾部会变成一条水平线——这意味着策略主动放弃了后半段可能继续亏损的暴露。 绝对回撤以初始资金为基准,相对回撤以历史最高资金为基准,分别对应 rmnd_abs() 和 rmnd_rel()。两种算法最终返回的都是生成利润序列的均值,差别只在「跌破」的参照线怎么画。 实测把 rmndmin 取 0.95、0.75、0.2 三档做优化:值越小,回撤越被容忍,优化结果越贴近纯利润最大化,本质是大数定律在样本量够大时的收敛。图上不容易一眼看穿,是因为纵轴被统一缩放到同维度,且小阈值带来的深回撤有滞后性。 把 rmndmin 也丢进参数优化器,跑出的最佳点是 0.55。此时回撤可能吞掉账户余额近一半,实盘很难直接用,但它暴露了一个事实:回撤已经很大的时候继续硬扛,大概率没用。这正好呼应「斩断亏损,让利润奔跑」的另一半。 下面两段是 MQL5 原生实现,逻辑不复杂但容易写错比较基准。rmnd_abs 里用 cn[n]<rmndmin 直接判死,rmnd_rel 则维护峰值 x,用 cn[n]/x<rmndmin 才断: double rmnd_abs(double &k[]) { 若 rmndmin 不在 (0,1) 区间直接返回 0,避免非法参数污染均值; 申请 km 与 cn[NSAMPLES] 数组,nk 为输入收益率序列长度; 对每次抽样 sample(k,km) 后 cn 从 1.0 起乘各笔收益 km[i]; 一旦 cn[n]<rmndmin 就 break 终止该序列,最后 cn[n]-=1 得净收益; 返回所有抽样净收益的 MathMean。} double rmnd_rel(double &k[]) {同样先拦 rmndmin 非法值,x 记录当前序列峰值资金; cn[n] 乘序中若创新高则更新 x,否则用 cn[n]/x<rmndmin 判破; 相对法以峰值而非起点为锚,更贴近实盘「从最高点回撤多少」的体感; 返回均值前同样减 1 得到相对净收益均值。} 外汇与贵金属杠杆高,这类回撤截断逻辑在 MT5 里跑前务必用历史分笔数据验一遍峰值更新有没有漏掉跳空。

MQL5 / C++
class="type">class="kw">double rmnd_abs(class="type">class="kw">double &k[])
  {
  if (rmndmin<=class="num">0.0||rmndmin>=class="num">1.0) class="kw">return class="num">0.0;
  class="type">class="kw">double km[],cn[NSAMPLES];
  class="type">int nk=ArraySize(k);
  ArrayResize(km,nk);
  for(class="type">int n=class="num">0;n<NSAMPLES;++n)
    {
    sample(k,km);
    cn[n]=class="num">1.0;
    for(class="type">int i=class="num">0;i<nk;++i)
      {
      cn[n]*=km[i];
      if(cn[n]<rmndmin) class="kw">break;
      }
    cn[n]-=class="num">1.0;
    }
  class="kw">return MathMean(cn);
  }
class="type">class="kw">double rmnd_rel(class="type">class="kw">double &k[])
  {
  if (rmndmin<=class="num">0.0||rmndmin>=class="num">1.0) class="kw">return class="num">0.0;
  class="type">class="kw">double km[],cn[NSAMPLES],x;
  class="type">int nk=ArraySize(k);
  ArrayResize(km,nk);
  for(class="type">int n=class="num">0;n<NSAMPLES;++n)
    {
    sample(k,km);
    x=cn[n]=class="num">1.0;
    for(class="type">int i=class="num">0;i<nk;++i)
      {
      cn[n]*=km[i];
      if(cn[n]>x) x=cn[n];
      else if(cn[n]/x<rmndmin) class="kw">break;
      }
    cn[n]-=class="num">1.0;
    }
  class="kw">return MathMean(cn);
  }

用 WMW 标准卡住利润分布的漂移

价格行为随时可能变脸,但 EA 的获利模型得扛得住——至少在行情不稳的阶段里,前后交易样本的利润分布不能差太离谱。把交易序列切成前半段和后半段,两段越接近,优化评分越高,这个近似度靠 Wilcoxon-Mann-Whitney(WMW)标准来量:它测的是两个样本互相偏移多少,偏移越小,标准值越靠近 1。 直接用 WMW 标准有个坑:它可能把净亏的参数为优。比如跑 10 组,挑 WMW 最高的两组,一组赚钱一组亏钱,标准本身分不出来。所以更务实的做法是,先筛 WMW 靠前的子集,再在里面捞利润最大的那个,而不是单看分布相似度。 文件里还塞了个「WMW × 利润」的复合标准。实测下来,它和纯按利润优化长得很像,等于多绕一圈,实用性存疑。WMW 更适合拿来比两个 EA 同周期、或同一个 EA 跨周期的表现,只是怎么比才严谨还没定论。 下面这段是 WMW 与复合标准的 MQL5 实现,注意 frw_wmw 里 fwdsh 控制前后段切分比例,低于最小成交数 NDEALSMIN 直接返回 0,避免小样本瞎评分。 double frw_wmw(double &k[]) {

if (fwdsh<=0.0fwdsh>=1.0) return 0.0;

int nk=ArraySize(k), nkf=(int)(fwdsh*nk), nkp=nk-nkf;

if(nkf<NDEALSMINnkp<NDEALSMIN) return 0.0;

double u=0.0; for (int i=0; i<nkp; ++i) for (int j=0; j<nkf; ++j) if(k[i]>k[nkp+j]) ++u; return 1.0-MathAbs(1.0-2.0*u/(nkf*nkp)); }

逐行拆解:函数先排除切分比例越界;按 fwdsh 把数组 k 拆成前 nkp 笔与后 nkf 笔;若任一半低于最小成交数则返回 0;双重循环统计前段严格大于后段的配对次数 u;最终映射成 1 减1-2u/(nkf*nkp),值越接近 1 两段分布越重合。

double frw_wmw_prf(double &k[]) { int nk=ArraySize(k); double prf=1.0; for(int n=0; n<nk; ++n) prf*=k[n]; prf-=1.0; if(prf>0.0) prf*=frw_wmw(k); return prf; } 逐行拆解:先对 k 所有元素连乘再减 1 得到利润因子 prf;仅当 prf 为正(即整体盈利)才乘上 WMW 值,亏损方案直接以负或零值暴露,不会被分布相似度掩盖。外汇与贵金属杠杆高,这类分布检验只降低过拟合概率,不承诺任何收益。

MQL5 / C++
class="type">class="kw">double frw_wmw(class="type">class="kw">double &k[])
  {
  if (fwdsh<=class="num">0.0||fwdsh>=class="num">1.0) class="kw">return class="num">0.0;
  class="type">int nk=ArraySize(k), nkf=(class="type">int)(fwdsh*nk), nkp=nk-nkf;
  if(nkf<NDEALSMIN||nkp<NDEALSMIN) class="kw">return class="num">0.0;
  class="type">class="kw">double u=class="num">0.0;
  for (class="type">int i=class="num">0; i<nkp; ++i)
    for (class="type">int j=class="num">0; j<nkf; ++j)
      if(k[i]>k[nkp+j]) ++u;
  class="kw">return class="num">1.0-MathAbs(class="num">1.0-class="num">2.0*u/(nkf*nkp));
  }
class="type">class="kw">double frw_wmw_prf(class="type">class="kw">double &k[])
  {
  class="type">int nk=ArraySize(k);
  class="type">class="kw">double prf=class="num">1.0;
  for(class="type">int n=class="num">0; n<nk; ++n) prf*=k[n];
  prf-=class="num">1.0;
  if(prf>class="num">0.0) prf*=frw_wmw(k);
  class="kw">return prf;
  }

「折衷标准比裸风险指标更靠谱」

直接把 VaR 或破产概率塞进优化标准,结果往往难看:交易量和交易次数会被系统性低估,等于自动砍掉了不少本可执行的信号。真正能用的,是一个同时吃下「风险」和「利润」两个变量的折衷函数——但这类函数的写法无穷多,选哪个版本成了新问题。 一种务实思路是按策略的激进程度来挑标准。趋势跟随型 EA 和区间震荡型 EA 对风险的敏感度完全不同,固定止盈系统和跟踪止损系统在利润分布上也不属同一类,硬套同一个标准会偏科。 有些标准擅长横比不同 EA,有些则更适合纵比同一 EA 在不同时段、不同参数集下的表现。蒙特卡洛若只喂真实历史,覆盖度有限;把价格历史做随机扰动副本一并灌进 EA 输入,稳定性检验会深一层,代价是计算量明显上升。外汇与贵金属品种波动剧烈,这类回测结论仅代表历史概率倾向,实盘仍属高风险。

◍ 随包附带的脚本与头文件

这套蒙特卡洛评估思路落进 MT5,靠的是两个文件:Moving_Average_mcarlo.mq5(8.18 KB)是改过的标准均线脚本,负责跑单;mcarlo.mqh(6.8 KB)是头文件,所有计算函数都在里头。 下载 ZIP 解压后,把两个文件丢进 MT5 的 MQL5/Scripts 与 MQL5/Include 对应目录,编译即可在策略测试器里跑。有读者在回溯测试中见过 Ontester 返回 4.3222,作者确认那是用作优化「自定义最大值」标准的数值,不是直接收益。 它只是基于拉尔夫-文斯假设做的粗略策略评估,且原作者点明只适用于固定头寸规模。外汇和贵金属波动剧烈、杠杆高风险大,用前先在小资金或模拟盘验证再谈参数。

常见问题

因为重采样是对原交易序列做有放回随机抽取,顺序和样本构成会变,回撤自然浮动。建议跑 500 次以上取分布中位数看典型风险。
会被尖峰拉歪。用中位数代替均值看中心趋势,能更稳地反映多数交易的实际利润水平。
可以。小布内置了重采样与分布诊断,打开品种页就能直接看中位数回撤和利润漂移预警,不用手动跑代码。
当账户剩余资金比例跌破阈值就强制停止开仓,切断越亏越补的回路。回测中该阈值设 0.7 时爆仓率明显下降。
用 Wilcoxon–Mann–Whitney 检验比较重采样利润与原序列,p 值小于 0.05 视为分布显著漂移,策略可能已失效。