蒙特卡洛方法在交易策略优化中的应用(基础篇)
用蒙特卡洛给 EA 利润挑刺
蒙特卡洛方法在 MT5 里不是用来算命,而是把历史行情打乱重排,看策略利润在随机扰动下还能不能站得住。2018 年 8 月那篇实测贴里,作者用概率性 EA 模型跑了几百次重采样,样本量约 5577 次观测,核心结论指向一个事实:历史回测好看,不代表随机散射下不崩。 具体做法分三层:利润鲁棒性对随机散射,看单次交易顺序打乱后净值曲线衰减;利润鲁棒性对回撤,统计最大回撤分布而非只看一个点;利润分布鲁棒性对价格不稳定性,用不同波动率缩放喂给同一套逻辑。外汇和贵金属杠杆高,这类测试暴露的是爆仓概率,不是收益承诺。 想在 MT5 验证,直接开策略测试器的自定义模式,把「使用随机延迟」和「重采样」勾上,跑 200 次以上取分位数。比对着一条曲线自我感动有用得多。
◍ 蒙特卡洛建模到底在算什么
蒙特卡洛方法本质上是一种用随机模拟给「带概率性质的对象」建模的思路。它把研究对象抽象成一套算法,在程序里用伪随机数发生器去逼近理论要求的随机分布,从而批量造出大量样本。 这种建模之所以好用,是因为算法结构简单:先按分布随机生成很多研究对象状态,对每个样本算一遍所需特征,就能从「只有一组真实特征」扩展出整个分布函数。样本越多,分布越有参考价值,但外汇和贵金属市场的高风险意味着:分布再漂亮也不代表下一笔就按它走。 最早的教学例子是布丰投针——往格子上随机丢针来估 Pi,后来轮盘赌这类天然随机数发生器也印证了「随机即概率」的来源。值得注意,蒙特卡洛不仅能处理随机对象,也能拿去近似普通积分,因为任意积分都能看成某个随机变量的数学期望。 落到交易上,我们要研究 EA 稳定性,就得先给 EA 工作过程配一个概率模型;模型越简单越容易算,但简化总会丢精度,这点心里要有数。
「把历史交易当成随机变量来建模」
开 MT5 跑 EA 前,大家习惯在报价历史上测和调。但过去那串成交结果,凭什么能预示将来?我们并不指望未来每笔单子顺序都一样,只是假定它们有“统计相似性”。用概率论把这直觉钉死:每笔交易结果视作某随机变量的实现,相似性由分布函数 F(x) 的接近程度衡量。过去数据的作用,就是帮我们反推未来利润分布的类型与可能区间。外汇与贵金属杠杆高,这种推断只代表概率倾向,不等于实盘必然重现。 最常用也最简的模型,是把每笔相对利润定义为 k = C1/C0(C0 建仓前资产、C1 平仓后资产),绝对利润就是两者差。假定所有交易利润相互独立、同分布,任务变成用历史样本还原 F(x)——标准的数理统计问题,答案永远是近似的。 一种做法是直接取经验分布函数,用离散近似:亏损按平均亏、获利按平均利,各占一个值。另一种用核密度估计做连续密度近似。实战里交易者不在乎函数长相,关心的是给定风险水平下可能落袋多少、利润能否持续,这些都能从分布里算出来。 具体建模用 bootstrap:历史利润序列 k1…kn,随机有放回抽 n 次生成新序列,跑几万条资金曲线(下图不同颜色仅示意,实际量级数万)。每条算终值利润,整体统计出可能结果分布。N 取 n 即可,N 再大经验分布近似精度反而掉。MT5 目前没有内建“随机 EA”常规接口,这套得自己写脚本跑。
把三种稳定性准则塞进 EA 的优化回路
构建优化标准的核心目的,是给 EA 一个可量化的目标函数,而不只是盯着总利润。未来利润不可知,但能用当前样本偏离利润的可能程度去评估——偏差越小,系统的利润越稳定,实盘中外汇与贵金属的高波动风险才更可控。 我们探讨三条路线:蒙特卡洛大样本下,取平均利润与离散程度之比,类似夏普比率,方差小代表不确定性低;把回撤约束进来,超过允许回撤就终止交易,该准则只算平均利润;用前后子样本同质性统计检验衡量利润持久性,思路接近前瞻测试。 演示用 MetaTrader 5 自带的 Moving Average.mq5,仅做小改:开头包含头文件,结尾用 OnTester 返回优化值。所有计算集中在 mcarlo.mqh 的 optpr(),它按 noptpr 参数分发到六个子函数(如 mean_sd、frw_wmw)。 回测设定统一为 EURUSD 2017 年春夏季、1 小时周期、1 分钟 OHLC 模式、遗传算法自定义优化。这不是为实盘做准备,而是验证理论——你打开 MT5 把头文件丢进 MQL5/Include/ 就能复跑。 下面这段是 optpr 与 setks 的骨架:optpr 先做参数与样本量守卫,再按 noptpr 分支;setks 从交易历史提取每笔盈亏相对资金曲线的增长系数存入 k[]。注意 k[nk-1]=1.0+hdprofit_full/capital 把利润归一化为复利因子,这是后续蒙特卡洛重采样的输入。
class="macro">#include <mcarlo.mqh> class="type">class="kw">double OnTester() { class="kw">return optpr(); class=class="str">"cmt">// 优化参数 } class="type">class="kw">double optpr() { if(noptpr<class="num">1||noptpr>NOPTPRMAX) class="kw">return class="num">0.0; class="type">class="kw">double k[]; if(!setks(k)) class="kw">return class="num">0.0; if(ArraySize(k)<NDEALSMIN) class="kw">return class="num">0.0; MathSrand(GetTickCount()); class="kw">switch(noptpr) { case class="num">1: class="kw">return mean_sd(k); case class="num">2: class="kw">return med_intq(k); case class="num">3: class="kw">return rmnd_abs(k); case class="num">4: class="kw">return rmnd_rel(k); case class="num">5: class="kw">return frw_wmw(k); case class="num">6: class="kw">return frw_wmw_prf(k); } class="kw">return class="num">0.0; } class="type">bool setks(class="type">class="kw">double &k[]) { if(!HistorySelect(class="num">0,TimeCurrent())) class="kw">return class="kw">false; class="type">uint nhd=HistoryDealsTotal(); class="type">int nk=class="num">0; class="type">class="kw">ulong hdticket; class="type">class="kw">double capital=TesterStatistics(STAT_INITIAL_DEPOSIT); class="type">long hdtype; class="type">class="kw">double hdcommission,hdswap,hdprofit,hdprofit_full; for(class="type">uint n=class="num">0;n<nhd;++n) { hdticket=HistoryDealGetTicket(n); if(hdticket==class="num">0) class="kw">continue; if(!HistoryDealGetInteger(hdticket,DEAL_TYPE,hdtype)) class="kw">return class="kw">false; if(hdtype!=DEAL_TYPE_BUY && hdtype!=DEAL_TYPE_SELL) class="kw">continue; hdcommission=HistoryDealGetDouble(hdticket,DEAL_COMMISSION); hdswap=HistoryDealGetDouble(hdticket,DEAL_SWAP); hdprofit=HistoryDealGetDouble(hdticket,DEAL_PROFIT); if(hdcommission==class="num">0.0 && hdswap==class="num">0.0 && hdprofit==class="num">0.0) class="kw">continue; ++nk; ArrayResize(k,nk,NADD); hdprofit_full=hdcommission+hdswap+hdprofit; k[nk-class="num">1]=class="num">1.0+hdprofit_full/capital; capital+=hdprofit_full; }