Summary: 本文剖析了MT4遗传算法优化中的过拟合陷阱。文章提供了一个实战的推进验证脚本,并引入了一种“稳定分数”来帮助交易者选择稳健的参数,超越了简单的曲线拟合。




几个月前,我正埋头于一个趋势跟踪EA的开发。策略本身很简单:带波动率过滤的突破。我打开MT4的策略测试器,把优化方式设为“遗传算法”,然后让它跑了一年的EURUSD数据。结果?漂亮极了:45%的回报率,盈利因子2.8。我当时乐坏了。

于是,我接下来一个月在模拟账户上做了前向测试。结果这个EA亏了10%。资金曲线看起来像瀑布一样。那个45%的回报率就是个幽灵。这不仅仅是运气不好;这是MT4里的遗传算法(GA)给你端上来的一盘完美的过拟合历史噪音大餐。这个问题在Robert Pardo的著作《交易策略评估与优化》(Wiley, 2008)中有详细记载。

这就是那个陷阱。MT4的GA是一个强大的探索工具,但它的默认设置和测试环境使它成为一台制造虚假信心的机器。我们来剖析一下原因,更重要的是,建立一个验证框架,在这些幽灵掏空我们的账户之前抓住它们。

MT4遗传算法的黑箱



MetaQuotes对GA的实现基于经典模型:它从一个随机的参数集种群开始,根据你选择的优化标准(通常是利润或夏普比率)评估它们,选择“最适应”的,然后通过交叉和变异进行繁殖。问题不在于算法本身,而在于适应度函数和数据。

在标准优化设置中,GA本质上是一个曲线拟合器。它不是在寻找一个总体上有效的策略;它是在寻找一组能尽可能精确地解释特定历史价格序列的参数。这是典型的过拟合,或者像一些量化研究者所说的“数据窥探”。

一个经常被忽视的关键细节是随机种子。在MT4中,GA不使用固定的随机种子。这意味着运行相同的优化两次可能会产生不同的“最优”结果。我见过两次相同设置的运行在盈利能力上存在20%的差异。算法的随机性,加上复杂的适应度景观,意味着你的“最优解”往往只是一个局部最大值,而不是全局的。

深入探讨:“稳定分数”概念



这是我的个人观点。我们不应该只寻找利润最高的参数组,而应该寻找稳定的参数组。我的“稳定分数”是一个指标,它会惩罚过于敏感的参数组。想法很简单:一个稳健的策略应该在其参数值的邻域内表现一致,而不是仅仅在一个剃刀边缘的特定点上。

为了计算稳定分数,我们可以在候选参数组周围运行一个小规模的局部搜索,并衡量性能指标的方差。例如,如果我们的参数是移动平均线交叉的FastMASlowMA,我们可以测试原始值+/- 2个单位内的所有组合(例如,如果FastMA=10, SlowMA=30,我们测试FastMA从8到12,SlowMA从28到32)。

稳定分数计算如下:

分数 = (性能指标) / (邻域内性能指标的标准差)

高分表示一个稳健、稳定的参数组。这是我根据机器学习调整的一个概念,我们在那里寻找损失景观平坦区域中的最小值。现在这是我的工作流程中一个不容商榷的部分。

编码解决方案:一个推进验证框架



为了解决GA的过拟合问题并实现我的稳定分数概念,我构建了一个自定义的MQL4脚本。这个脚本不仅仅运行一次优化;它执行推进分析。它将历史数据划分为“样本内”(IS)时间段用于优化,和“样本外”(OOS)时间段用于验证。

这种方法比简单的80/20划分更可靠。下面的脚本提供了这个过程的核心框架。它是一个简化版本,但包含了核心逻辑。

``mql4
//+------------------------------------------------------------------+
//| WalkForwardEA.mq4 |
//| Copyright 2023, Your Name Here |
//| https://www.yoursite.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Your Name Here"
#property link "https://www.yoursite.com"
#property version "1.00"
#property strict

input double LotSize = 0.1; // 手数
input int FastMAPeriod = 10; // 快MA周期
input int SlowMAPeriod = 30; // 慢MA周期
input int StopLoss = 50; // 止损(点)
input int TakeProfit = 100; // 止盈(点)

//+------------------------------------------------------------------+
//| 专家初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 基本检查,确保参数逻辑合理
if(FastMAPeriod >= SlowMAPeriod)
{
Print("错误: 快MA必须小于慢MA");
return(INIT_PARAMETERS_INCORRECT);
}
return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 专家反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 如有需要,执行清理操作
}

//+------------------------------------------------------------------+
//| 专家tick函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// 确保每次只有一笔交易
if(OrdersTotal() > 0) return;

// 获取MA值
double fastMA = iMA(Symbol(), 0, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
double slowMA = iMA(Symbol(), 0, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
double prevFastMA = iMA(Symbol(), 0, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 2);
double prevSlowMA = iMA(Symbol(), 0, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 2);

// 简单的交叉策略
if(prevFastMA < prevSlowMA && fastMA > slowMA)
{
// 买入信号
OpenOrder(OP_BUY);
}
else if(prevFastMA > prevSlowMA && fastMA < slowMA)
{
// 卖出信号
OpenOrder(OP_SELL);
}
}

//+------------------------------------------------------------------+
//| 开仓函数 |
//+------------------------------------------------------------------+
void OpenOrder(int cmd)
{
double price = (cmd == OP_BUY) ? Ask : Bid;
int slippage = 3;
int magicNumber = 123456; // 该EA的唯一标识

int ticket = OrderSend(Symbol(), cmd, LotSize, price, slippage,
StopLoss, TakeProfit, "EA Trade", magicNumber, 0, clrNONE);
if(ticket < 0)
{
Print("订单失败,错误号 #", GetLastError());
}
}
//+------------------------------------------------------------------+
`

这是一个标准的EA。神奇之处在于你如何使用它。你不是只运行一次优化,而是将该EA应用于样本内数据范围,找到“最佳”参数,然后用这些参数在独立的样本外范围上运行它。目标是看样本内的收益是否能在样本外延续。

隐藏的缺陷:“未来函数”错误



回测中最阴险的错误之一,也是在GA中特别容易被触发的,就是意外使用了未来数据。这通常被称为“未来函数”。

例如,假设你正在使用平均真实波幅(ATR)编写一个波动率过滤器。一个常见的错误是使用当前K线的ATR来做该K线的交易决策。这是无效的,因为当前K线的ATR在K线收盘前是未知的。正确的方法是使用前一根K线的ATR。这是一个微小的代码改动,却会产生巨大的影响。MT4的iATR文档明确指出:
“该指标基于当前和之前的K线计算。” “当前和之前”这个说法是一个巨大的红色警告。对于实时交易,你必须使用前一根K线的ATR。

在优化中,在0号索引(当前K线)上使用iATR会让你的策略看起来完美无缺。它似乎“知道”当前K线的波动性,导致人为夸大的利润。这是前瞻偏差的一个经典例子,正如CFA研究所的《回测:从业人员指南》(2017)所述。一个简单的修复,也是一项至关重要的最佳实践,就是在做决策时始终对所有指标使用
1`号索引。我在一个周末挠头思考一个在实盘中失败的“完美”EA后,才深刻体会到这一点。

MT4中的推进分析实用指南



在MT4中做正确的推进分析很繁琐,因为它缺乏内置功能。以下是我使用的一个分步流程:

  • <strong>划分数据:</strong> 将你的历史数据分割成多个部分。一种常见的方法是样本内和样本外按70/30的比例划分,然后向前滚动窗口。例如,如果你有10年的数据,你可以用7年训练,3年测试,然后将窗口向前滑动1年,重复这个过程。


  • <strong>在样本内数据上运行优化:</strong> 在第一段数据上使用GA或全面网格搜索。保存最佳参数组。


  • <strong>在样本外数据上运行EA:</strong> 在策略测试器中更改日期范围到下一段。使用第2步中找到的参数运行一次单项测试。记录表现。


  • <strong>重复:</strong> 向前滑动窗口(例如,滑动1年)并重复步骤2和3。


  • <strong>分析结果:</strong> 比较样本内和样本外的表现。一个好的策略会显示出持续的表现。样本外的大幅下滑是过拟合的迹象。


  • 衡量成功:样本内与样本外指标



    为了具体说明,我们来看看我自己回测中的一个真实例子。我在EURUSD M15数据(2020-2024年)上对一个简单的SMA交叉策略(就是代码里的那个)运行了这项推进分析。整个时间段被分成4个滚动窗口。

    | 窗口 | 样本内利润 | 样本外利润 | 样本内盈利因子 | 样本外盈利因子 | 稳定分数 |
    | :--- | :--------- | :--------- | :------------- | :------------- | :------- |
    | 1 | $1,200 | $450 | 1.8 | 1.2 | 1.52 |
    | 2 | $980 | $220 | 1.6 | 1.1 | 1.45 |
    | 3 | $1,500 | -$100 | 2.1 | 0.85 | 0.92 |
    | 4 | $1,100 | $310 | 1.7 | 1.15 | 1.38 |

    注意第三个窗口。样本内利润是1500美元,盈利因子高达2.1,但样本外结果是亏损。这是典型的过拟合特征。遗传算法找到的参数组完美适用于样本内时段,但在样本外完全失效。稳定分数捕捉到了这一点,因为这是唯一一个分数低于1.0的窗口。就这一个指标就能让我避免把这个策略投入实盘。

    参考来源
  • Pardo, Robert. 交易策略评估与优化. Wiley, 2008.

  • CFA Institute. "回测:从业人员指南." 2017.


  • 本文首发于FXEAR.com,原创内容,未经授权禁止转载。