蒙特卡洛方法在交易策略优化中的应用·进阶篇
用均匀随机做数组重采样
下面这段 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 观察抽样分布。 外汇与贵金属市场高杠杆、随机性极强,这类重采样仅用于样本扰动或集成测试,不代表任何方向概率优势,实盘使用须自行验证。
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 需先在全局定义采样次数:
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 里跑前务必用历史分笔数据验一遍峰值更新有没有漏掉跳空。
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.0 | fwdsh>=1.0) return 0.0; |
|---|
int nk=ArraySize(k), nkf=(int)(fwdsh*nk), nkp=nk-nkf;
| if(nkf<NDEALSMIN | nkp<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 值,亏损方案直接以负或零值暴露,不会被分布相似度掩盖。外汇与贵金属杠杆高,这类分布检验只降低过拟合概率,不承诺任何收益。
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,作者确认那是用作优化「自定义最大值」标准的数值,不是直接收益。 它只是基于拉尔夫-文斯假设做的粗略策略评估,且原作者点明只适用于固定头寸规模。外汇和贵金属波动剧烈、杠杆高风险大,用前先在小资金或模拟盘验证再谈参数。