零售算法交易中最肮脏的秘密之一,就是MT4完美优化后的回测与残酷的实盘前测之间的鸿沟。问题几乎总是过拟合,而通常的罪魁祸首就是遗传算法(GA)优化器本身。遗传算法在寻找多维空间中的局部最大值方面表现出色,但它们在这一点上做得太好了。它们会很乐意找到那套完美的、脆弱的参数组合,这套组合在2018年表现得天衣无缝,然后在2020年就让你的账户爆仓。
这并非是对自动化优化的全盘否定,而是呼吁一种更细致的方法。我想逐步介绍一个将稳健性置于首位的工作流程。它涉及到放弃“优化”选项卡中默认的“余额”或“夏普比率”标准,转而使用自定义指标,并实施一个我经实践检验更为可靠的手动迭代过程。
遗传算法“最优”结果的问题
标准的MT4优化器在使用遗传算法时,本质上是一个复杂的曲线拟合工具。它评估参数组合的一个子集,“培育”最佳组合并进行变异以找到最优点。问题在于,适应度函数(最大化余额、盈利因子等)是脱离历史背景的。它不关心策略是否在特定波动时期存活下来;它只关心最终的数字。
让我们看一个常见场景。你正在优化一个简单的移动平均线交叉EA。你的参数是
FastMA和SlowMA。遗传算法可能会收敛到一个它们之间跨度仅为2根K线的解(例如,快线=14,慢线=16)。这会产生大量信号,其中许多会是假突破,但在用于优化的特定历史数据中,它完美地捕捉到了趋势。这就是经典的“过度优化”陷阱。该策略十分脆弱。我的方法是优化稳定性而不是表现。我想要一组位于盈利“高原”上的参数,而不是一个尖锐的峰值。这意味着我们需要分析参数空间,而不仅仅是一个单点。
手动、多阶段优化过程
这是我多年来改进的一个过程。它工作量稍大,但其前测结果始终优于遗传算法的单个最优运行结果。
阶段1:使用自定义指标进行粗粒度网格搜索
在这个阶段,先忘记内置优化器。我们将使用一个脚本来运行网格搜索,并将结果存储在CSV文件中。这让我们可以定义自己的适应度函数。我最喜欢的是盈利因子 / 最大回撤比率。
代码:MQL4网格搜索框架
``
mql4
//+------------------------------------------------------------------+
//| GridSearchSkel.mq4 |
//| Copyright 2023, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| 自定义适应度函数 - 盈利因子除以最大回撤 |
//+------------------------------------------------------------------+
double CustomFitness(double profitFactor, double maxDrawdownPercent) {
if (maxDrawdownPercent == 0.0) return profitFactor;
// 大幅惩罚高回撤
return profitFactor / (1.0 + maxDrawdownPercent 10.0);
}
//+------------------------------------------------------------------+
//| 脚本程序启动函数 |
//+------------------------------------------------------------------+
void OnStart() {
int handle = FileOpen("GridSearch_Results.csv", FILE_WRITE|FILE_CSV|FILE_READ, ",");
if (handle < 0) {
Print("创建文件失败. 错误代码: ", GetLastError());
return;
}
// 写入CSV表头
FileWrite(handle, "FastMA", "SlowMA", "ProfitFactor", "MaxDD%", "Fitness");
// 定义参数范围
int fastStart = 5, fastEnd = 25, fastStep = 2;
int slowStart = 20, slowEnd = 100, slowStep = 5;
// 这是一个框架。在实际中,你会在这里调用一个回测函数。
// 以下循环模拟了网格搜索过程。
for (int fast = fastStart; fast <= fastEnd; fast += fastStep) {
for (int slow = slowStart; slow <= slowEnd; slow += slowStep) {
if (slow <= fast) continue; // 跳过无效组合
// --- 回测执行占位符 ---
// double profitFactor = RunBacktest(fast, slow);
// double maxDD = GetMaxDrawdown();
// --- 占位符结束 ---
// 模拟演示数据
double profitFactor = (rand() % 200) / 100.0;
double maxDD = (rand() % 30) / 100.0;
double fitness = CustomFitness(profitFactor, maxDD);
FileWrite(handle, fast, slow, DoubleToString(profitFactor, 2),
DoubleToString(maxDD 100, 2), DoubleToString(fitness, 3));
}
}
FileClose(handle);
Print("网格搜索完成。结果已写入 'GridSearch_Results.csv'");
}
//+------------------------------------------------------------------+
`
代码注释:
#property strict:必需品。它强制进行正确的变量类型定义并减少错误。
CustomFitness:这是关键所在。通过将盈利因子除以缩放后的回撤,我们大幅倾向于平滑的策略。盈利因子=2.0且回撤=5%的策略得分为 2.0 / (1.5) = 1.33。盈利因子=3.0但回撤=20%的策略得分为 3.0 / (3.0) = 1.0。平滑的策略胜出。这直接反驳了默认优化器的逻辑。
FileWrite 循环:这比内置优化器的输出强大得多,因为你可以随后在Python或Excel中分析这个CSV文件,寻找高适应度评分的“高原”,而不仅仅是单个最高点。
阶段2:分析“高原”
运行网格搜索后,我将结果绘制成热力图。我寻找的不是单个最高峰。相反,我寻找的是高适应度分数最大的连续区域。这个“高原”代表了对微小变化具有稳健性的参数集。一个在快线=14、慢线=45时表现良好,并且在快线=16、慢线=50时同样有效的策略,比一个仅在14、45时有效的策略更可靠。
关于“推进分析”概念的一点说明: 这个“高原”本质上是一个手动的、样本内的合理性检查。真正的考验是样本外测试。
阶段3:推进分析(真正的考验)
这是整个过程中最关键的部分,也经常被误解。人们进行一次优化,然后进行一次前测。这并不严谨。适当的推进分析涉及滚动窗口。
<strong>样本内(IS):</strong> 在2018-2019年的数据上进行优化。
<strong>样本外(OOS):</strong> 在2020年第一季度的数据上测试优化后的参数。
<strong>滚动推进:</strong> 现在,在2018-2020年(包括样本外期间)的数据上重新优化,并在2020年第二季度进行测试。
继续这个过程。
其理念是,如果你的优化过程是合理的,那么样本外表现平均而言应达到样本内表现的约60-70%。如果表现大幅落后,则说明你的策略过拟合了。
解决“未来函数”的陷阱
这是EA开发中最容易被忽视的细节。 “未来函数”是指在EA执行时,使用尚未收盘的K线数据的任何指标或计算。最常见的罪魁祸首是 OnTick() 函数中的 iClose(NULL, 0, 0)。在新K线的开盘价到达时,iClose(NULL, 0, 0) 是前一根K线的收盘价,这没问题。然而,当使用高时间框架指标时(例如,在M15图表上使用H4指标),索引 0 指的是当前仍在形成的更高时间框架K线。这可能导致你的EA“看到”未来。
一个具体且棘手的例子: 你有一个基于H4收盘价进行交易的EA。你设置了一个检查条件:if (iClose(NULL, PERIOD_H4, 0) > iMA(NULL, PERIOD_H4, 20, 0, MODE_SMA, PRICE_CLOSE, 0))。在这一行中,iClose(NULL, PERIOD_H4, 0) 是当前、未收盘的H4 K线的当前价格。它是一个移动目标,并且明显是一个未来函数。正确的方法是始终检查已收盘的K线:iClose(NULL, PERIOD_H4, 1)。这是许多高级程序员在试图加快执行速度或简化代码时常犯的错误。
我的独家观点是: 我认为业内过度关注优化器作为过拟合的唯一来源。根据我的经验,从回测到实盘失败的案例中,有80%是由于这些隐藏的未来函数造成的,而非优化算法本身。优化器只是一个放大镜;它是在放大你代码中已有的缺陷。首先修复逻辑。对每一个 iClose 和 iOpen 调用进行审查,并验证你操作的是哪根K线。
关于跨平台迁移(MQL4到MQL5)的说明
稳健优化的概念是与平台无关的。然而,迁移你的自定义适应度逻辑才是难点所在。在MQL5中,OnTester() 函数让你能直接访问优化过程,允许你返回一个自定义的 double 值。这远优于MQL4的 TesterStatistics()。
我的“高原”分析在MQL5的 OnTesterInit() 和 OnTesterPass() 函数中实现起来要容易得多,你可以在测试器框架内直接构建热力图。迁移时常见的错误是试图在MQL5中复制MQL4的 GlobalVariable 技巧。千万不要。拥抱 OnTester 事件结构;它更清晰、更快速。
成功的真正衡量标准
很久以前我就不再关心高的“优化得分”了。真正的衡量标准是你的推进分析中样本外收益与样本内收益的比率。如果在多个滚动窗口中该比率持续高于0.6,那么你就拥有一个稳健的系统。其他的都只是噪音。
参考文献:
Pardo, Robert. The Evaluation and Optimization of Trading Strategies. John Wiley & Sons, 2008.
MQL5关于 OnTester和自定义优化标准的参考文档: https://www.mql5.com/zh/docs/basis/ontester
本文首发于FXEAR.com,原创内容,未经授权禁止转载。
``