MetaTrader 5 中的自定义前瞻优化(基础篇)
◍ 用 MQL 把前瞻优化跑成自动流水线
MT5 内置测试器能做历史数据参数优化,但官方只提供单次「优化+前瞻」的内置模式,没法一键把这套动作按固定时间偏移反复跑。实际 EA 运行是优化段和交易段按频率循环,想验证长期不亏,至少得把测试器重复启动多次、从过去滑到虚拟未来做滚动前瞻。 原厂测试器不开放按钮触发连续前瞻,所以得靠外部工具或纯 MQL 自己写调度。2017 年社区里有人实测:手动做完整轮次前瞻,单次优化区间加验证区间跑完要重复十几轮,纯手工点测不现实。 可行做法是写个 MQL 函数库,挂到任意 EA 源码里,由 EA 自身调测试器接口自动生成前瞻报告,免去重复人工操作。代价是要会改 EA 代码,把库接进去。外汇和贵金属品种波动突变多,这种滚动前瞻只降低过拟合概率,不保证实盘能活。
「把样本内窗口和样本外步长拆开跑」
做前瞻优化前先厘清几个词:优化区间就是样本内数据,后续测试区间是样本外数据。为方便描述,用“窗口 W”指代样本内优化窗口,“步长 S”指代紧随其后的样本外测试段。核心约束是总周期 D 远大于 W,且 W 又远大于 S,典型比例可设为 D 包含一个 W 加十几个 S。 机制并不复杂:在 MT5 测试器里选好总区间 D,EA 只允许在当前窗口 W 内交易并优化参数,把紧接的 S 段交易结果单独存下来不计入优化。窗口从起点 B 开始,按 S 步长向右平移多次,直到右边界抵到 D 的末端。每平移一次(索引 i),就得到一组该窗口的最优参数和对应的样本外实绩。 一个具体例子:W 取 3 个月、S 取 1 个月、D 取 12 个月,则平移步数 N = (12-3)/1 = 9。平移 0 到 8 时窗口右侧都有 S 可验,平移 9 时窗口已贴末端、无法再做样本外测试,此时该步最优参数组可直接用于实盘参考。外汇与贵金属属高风险品种,这套参数仅代表历史区段拟合结果,实盘表现可能偏离。 要让测试器自动搜参,需给 EA 加专用输入:窗口大小 W、步长 S、平移索引号。在“自定义优化条件”里用 OnTester 事件仅返回 W 内交易的评价指标,同时把 S 段结果写文件。优化完遍历各平移 i,就能拼出整段 D 的前瞻报告。若把 W 和 S 自身也丢进优化列表,让测试器搜最佳窗口与步长组合,就叫集群前瞻分析。 除滚动前瞻(窗口滑动)外,还有锚定前瞻:W 起点固定在 B,每次平移时窗口向右扩张把上轮 S 吞进样本内。例如 1—3 月优化、4 月测;接着 1—4 月优化、5 月测。MT5 的 WalkForwardOptimizer 库比 MT4 版更顺手:单模块收集数据出报告、免手工挪文件、参数名自动绑定、支持云代理多线程,底层靠 MQL5 API 的扩展能力。
窗口与步进的元参数怎么配
做前瞻优化(WFO)时,EA 头文件里那批带 wfo_ 前缀的输入就是元参数,它们只管优化过程、不和交易逻辑混在一起。wfo_windowSize 和 wfo_stepSize 直接吃 WFO_TIME_PERIOD 枚举,基本单位是天,测试器本身只认整天精度,想做日内柱线级得另搞替代方案。 一周固定 7 天、一月 30 天、一季 90 天、半年 180 天、一年 360 天,都是写死的常数,不会去对齐日历边界。好处是窗口随便平移都不用管几月几号,代价也很明显:若选 month(30 天)从中旬起算,后面每次步进都顺延到中旬,日期会慢慢漂。 选 custom 后,窗口用 wfo_customWindowSizeDays 按天填,步进用 wfo_customStepSizePercent 按窗口百分比填。这里有个坑:最小窗口 10 天、步进从 10% 起每步加 10%,降到 10% 以下就没意义了(那是 1 天);若窗口 30 天、步进设 5%,第二次迭代算 15% 得 4.5 天,库不会崩但前瞻步进对不上,计算准确性会偏。 wfo_stepOffset 用来排步进顺序,增量必须恒为 1;wfo_outputFile 决定 csv 落盘位置(MT5 在 MQL5/Files)。评估方法 wfo_estimation 默认 wfo_built_in_loose,是夏普+盈利因子+盈利大小+交易数的复合;若填 wfo_expression,得在 wfo_formula 写自定义方程。两个都设 none 则整库停用。 代码里枚举和宏定义把时间换成了秒,方便直接喂给测试器事件函数。注意头文件只声明类型和元参数,真要跑起来还得把 wfo_OnInit / wfo_OnTick / wfo_OnTester 接进 EA 源码,不然优化窗口根本不会动。
input WFO_TIME_PERIOD wfo_windowSize = year; input class="type">int wfo_customWindowSizeDays = class="num">0; input WFO_TIME_PERIOD wfo_stepSize = quarter; input class="type">int wfo_customStepSizePercent = class="num">0; input class="type">int wfo_stepOffset = class="num">0; input class="type">class="kw">string wfo_outputFile = ""; input WFO_ESTIMATION_METHOD wfo_estimation = wfo_built_in_loose; input class="type">class="kw">string wfo_formula = ""; class="macro">#define DAYS_PER_WEEK class="num">7 class="macro">#define DAYS_PER_MONTH class="num">30 class="macro">#define DAYS_PER_QUARTER(DAYS_PER_MONTH*class="num">3) class="macro">#define DAYS_PER_HALF(DAYS_PER_MONTH*class="num">6) class="macro">#define DAYS_PER_YEAR(DAYS_PER_MONTH*class="num">12) class="macro">#define SEC_PER_DAY(class="num">60*class="num">60*class="num">24) class="macro">#define SEC_PER_WEEK(SEC_PER_DAY*DAYS_PER_WEEK) class="macro">#define SEC_PER_MONTH(SEC_PER_DAY*DAYS_PER_MONTH) class="macro">#define SEC_PER_QUARTER(SEC_PER_MONTH*class="num">3) class="macro">#define SEC_PER_HALF(SEC_PER_MONTH*class="num">6) class="macro">#define SEC_PER_YEAR(SEC_PER_MONTH*class="num">12) class="macro">#define CUSTOM_DAYS -class="num">1 enum WFO_TIME_PERIOD { none = class="num">0, year = DAYS_PER_YEAR, halfyear = DAYS_PER_HALF, quarter = DAYS_PER_QUARTER, month = DAYS_PER_MONTH, week = DAYS_PER_WEEK, day = class="num">1, custom = CUSTOM_DAYS }; enum WFO_ESTIMATION_METHOD { wfo_built_in_loose, wfo_built_in_strict, wfo_profit, wfo_sharpe, wfo_pf, wfo_drawdown, wfo_profit_by_drawdown, wfo_profit_trades_by_drawdown, wfo_average, wfo_expression }; class="macro">#class="kw">import "WalkForwardOptimizer.ex5" class="type">void wfo_setEstimationMethod(WFO_ESTIMATION_METHOD estimation, class="type">class="kw">string formula); class="type">void wfo_setPFmax(class="type">class="kw">double max); class="type">void wfo_setCloseTradesOnSeparationLine(class="type">bool b); class="type">void wfo_OnTesterPass();
◍ 把步进优化器嵌进EA的生命周期
想在MT5里做 walk-forward 优化,得先把那套外部函数的钩子接进EA标准事件。头文件里暴露了 wfo_OnInit、wfo_OnTick、wfo_OnTester 等几个入口,缺一个,回测就跑不出前瞻结果。 OnInit 里最关键是最后一行 wfo_OnInit(...),它吃四个参数:窗口尺寸、步长、步偏移、自定义天数与步长百分比,全部来自头文件输入变量。前面几行都是可选的,比如 wfo_setPFmax(100) 把盈利因子上限锁在100,省缺是 DBL_MAX;不写 wfo_setCloseTradesOnSeparationLine 就按 false 处理。 OnTick 的返回值要判:-1 表示窗口还没开始,+1 表示前瞻测试段已结束,这两种状态直接 return 不交易。只有落在区间内才跑你自己的策略代码,这是避免用未来数据作弊的硬隔离。 测试器侧四个回调全标了「必修」:OnTesterInit 传日志路径,OnTesterDeinit 收尾,OnTesterPass 喂每代结果,OnTester 返回 wfo_OnTester() 的评价值。外汇与贵金属品种波动大、滑点敏感,接完这套建议先用 EURUSD 十五分钟线跑一遍样本内窗口,确认日志有输出再上真品种。
class="type">int wfo_OnInit(WFO_TIME_PERIOD optimizeWindow, WFO_TIME_PERIOD optimizeStep, class="type">int optimizeStepOffset, class="type">int optimizeCustomW, class="type">int optimizeCustomS); class="type">int wfo_OnTick(); class="type">class="kw">double wfo_OnTester(); class="type">void wfo_OnTesterInit(class="type">class="kw">string optimizeLog); class="type">void wfo_OnTesterDeinit(); class="macro">#class="kw">import class="macro">#include <WalkForwardOptimizer.mqh> ... class="type">int OnInit(class="type">void) { class=class="str">"cmt">// 您的工作代码在此 ... class=class="str">"cmt">// 可选, 省缺为 wfo_built_in_loose wfo_setEstimationMethod(wfo_estimation, wfo_formula); class=class="str">"cmt">// 可选, 省缺为 DBL_MAX wfo_setPFmax(class="num">100); class=class="str">"cmt">// 可选, 省缺为 false class=class="str">"cmt">// wfo_setCloseTradesOnSeparationLine(true); class=class="str">"cmt">// 必修, 来自头文件的所有参数 class="type">int r = wfo_OnInit(wfo_windowSize, wfo_stepSize, wfo_stepOffset, wfo_customWindowSizeDays, wfo_customStepSizePercent); class="kw">return(r); } class="type">void OnTesterInit() { wfo_OnTesterInit(wfo_outputFile); class=class="str">"cmt">// 必修 } class="type">void OnTesterDeinit() { wfo_OnTesterDeinit(); class=class="str">"cmt">// 必修 } class="type">void OnTesterPass() { wfo_OnTesterPass(); class=class="str">"cmt">// 必修 } class="type">class="kw">double OnTester() { class="kw">return wfo_OnTester(); class=class="str">"cmt">// 必修 } class="type">void OnTick(class="type">void) { class="type">int wfo = wfo_OnTick(); if(wfo == -class="num">1) { class=class="str">"cmt">// 等到窗户开始 class=class="str">"cmt">// 不会交易 class="kw">return; } else if(wfo == +class="num">1) { class=class="str">"cmt">// 等待测试窗口前瞻测试结束后 class=class="str">"cmt">// 不会交易 class="kw">return; } class=class="str">"cmt">// 您的工作代码在此 ... }