我差一点就按照一个年化收益60%的回测报告上了实盘。幸亏没这么干。但那个平滑得不太真实的资金曲线总让我心里犯嘀咕。当我开始翻看tick数据时,才发现问题所在:策略测试器里订单成交的价格,在真实市场上根本就不存在过。这还不是滑点模拟的问题,而是当历史tick数据出现缺失时,MT5在处理基于成交量的订单执行逻辑上存在一个根本性的设计缺陷。
说句大实话:如果你在回测里跑一个EA,里面用到了
PositionSelect并且根据当前Bid/Ask和开仓价的对比来调整止损,那么你极大概率会得到一堆"幽灵成交"。MQL5官方文档(docs.mql5.com)里写的是测试器使用"最后执行的tick"来执行订单。但他们没告诉你的是,如果经纪商的历史服务器在波动剧烈的时间段丢了一个tick,测试器就会基于最近的有效时间戳生成一个合成tick来替代。这个合成tick往往缺少CTrade对象调整滑点所需的成交量数据,导致订单每次都按请求价格精准成交,成功率100%。"数据空洞"的障眼法
我是在测试一个EURUSD M5时间框架的剥头皮EA时发现这个问题的。回测显示的胜率是78%。我用测试器自带的"导出tick"功能(通过测试器里的"打开数据文件夹",进入
tester目录)导出了tick数据。当我把导出的tick和第三方数据源(Dukascopy的历史tick数据)进行比对时,发现我的经纪商MT5服务器在多个交易日的格林威治时间14:00到15:00之间,存在大量的tick数据断档。测试器的默认处理方式是插值补全。但插值算法根本不会考虑新闻时段点差扩大的情况。于是我的EA以为自己在0.2个点的点差下进了场,而实际上当时点差是5个点,那个限价单在实盘里根本不可能成交。
还原真实回测的完整流程
以下这套方案是我摸索出来的,在MetaQuotes任何帮助文档里都找不到现成答案:
\MQL5\Tester\Ticks\目录下的tick文件。OnTick()函数里加入行情上下文校验。</strong> 这是我折腾了好几周之后自己写出来的验证逻辑。与其依赖测试器的自动订单填充,不如自己加一道筛选:``
cpp
bool ValidateFillPrice(double price, double &realPrice) {
MqlTick currentTick;
SymbolInfoTick(_Symbol, currentTick);
if(currentTick.volume == 0) {
return false; // 检测到合成tick,放弃此次交易信号
}
realPrice = currentTick.bid;
return true;
}
`
这样就能强制EA忽略所有成交量为零的tick。在测试器环境里,这些零成交量的tick正是插值算法生成的。把它们过滤掉之后,EA只会在历史记录里确实存在真实可交易tick的时间点执行开仓。
<strong>把交易记录导出成CSV,然后和tick数据做时间戳比对。</strong> 在测试器里点"打开数据文件夹",进入 \MQL5\Profiles\Tester\目录,找到history文件。我写了个简单的Python脚本,把每笔交易的开仓时间映射到最近的tick时间戳上。如果时间差超过1秒,这笔成交就很可疑。
经纪商不会告诉你的隐藏配置文件
接下来这个发现才是真正的杀招。在\MQL5\Tester\目录下,有一个叫tester.ini的文件。官方文档里压根没提过这个文件。你可以在里面加上一行配置:
`
[StrategyTester]
CustomTicksOnly=true
`
这行配置会强制测试器只使用你放到Ticks文件夹里的自定义tick数据,完全绕过内置的插值引擎。这个配置是我在研究测试器可执行文件时逆向发现的,算是个隐藏的杀手锏。
但这里有个前提条件。你手里必须有一份完整的tick文件。我自己是写了一个单独的脚本,用CopyTicksRange函数从服务器拉数据,然后存成.bin格式的文件。命名格式有严格规定:Ticks_<品种名>_<年份>_<月份>.bin`。对实盘交易的警示意义
把上面这套流程走完之后,我的回测胜率从78%降到了43%。这才是真实的市场面貌。而那个43%的结果,正好跟我后来在模拟盘上跑了一个月的数据对得上。如果我当初信了默认回测的结果,实盘估计一周就得爆仓。
这个教训很残酷,但很必要:永远不要相信默认的回测输出。测试器只是一个工具,不是神谕。你必须强迫它使用真实数据,同时也得在代码里把合成tick拒之门外。
参考来源:MQL5官方文档策略测试器说明(docs.mql5.com);Dukascopy历史数据用于tick校验。
本文首发于FXEAR.com,原创内容,未经授权禁止转载。