Summary: MT4策略测试器的遗传算法优化经常产生欺骗性的权益曲线。本文揭示了隐藏的过拟合机制,提供了一个自定义验证脚本,并提出用稳健性得分替代默认的利润因子。
刚开始做EA优化那会儿,我把策略测试器里的遗传算法(GA)当成万能灵药。扔进去一堆参数,让电脑跑一宿,第二天醒来就能看到一条完美无瑕的权益曲线。然后一上前向测试,现实就抽我嘴巴子。
MT4默认的遗传算法本身并不蠢,它在搜索参数空间方面其实相当高效。问题不在于算法本身,而在于评估函数。在单一历史分笔数据上最大化利润因子或净利润,在数学上就等价于用高次多项式去拟合噪声。
隐藏的反馈回路
实际运行时的真实情况是这样的。MT4里的GA使用你的优化参数的二进制表示,交叉和变异算子负责探索搜索空间。但适应度分数的计算,是基于*完全相同*的用于初始种群评估的价格序列。
这就产生了一个恶性反馈回路:
1. 某组参数在特定的历史波动率集群上产生了一串幸运的交易序列。
2. GA选择这组参数进行重组。
3. 后代继承了适应那个特定波动率集群的特性。
4. 经过20-30代之后,整个种群收敛到的参数,本质上已经是“训练”用于你的测试周期里那2-3个特定市场状态了。
MetaQuotes关于GA的官方文档(`docs.mql4.com/ru/optimization/geneticalg`)里提到“该算法使用历史数据评估适应度”。他们没有强调的一点是,如果不做严格的样本外验证,你本质上就是用20-30个自由度去拟合一个只有200-300笔交易的数据集。信噪比低得可怜。
前向测试的迷惑性
大多数交易者做前向测试的方法是,把优化后的EA放到后续的时间段里跑。如果权益曲线跌了,他们就认为优化失败了。这是一个非黑即白、没什么帮助的结论。
我写了一个简单的MQL4脚本,用来追踪参数稳健性随时间衰减的情况。我不做单一的前向测试,而是把历史数据分成5个相互重叠的时间窗口。对每个窗口,我都运行GA并记录前10组参数。然后我把每组参数拿到*所有*其他窗口里去测试。
结果呢?窗口1里的“最佳”参数组在窗口3上的表现,往往还不如窗口1里排名第5或第6的那一组。排名靠前的参数过拟合了。真正稳健的参数组,其实藏在排名的中段。
一个实用的稳健性检查
下面这段代码现在是我每次优化流程里必加的东西。它能在优化过程中直接算出一个简单的稳健性得分(RS),不需要额外做前向测试。
```mql4
//+------------------------------------------------------------------+
//| 稳健性得分计算 |
//| 此函数评估参数在不同子周期内的稳定性 |
//+------------------------------------------------------------------+
double CalculateRobustnessScore(int ¶m1, int ¶m2, double ¶m3)
{
// 把测试周期分成3个子周期
datetime startTime = iTime(Symbol(), Period(), Bars - 1);
datetime endTime = iTime(Symbol(), Period(), 0);
datetime mid1 = startTime + (endTime - startTime) / 3;
datetime mid2 = startTime + 2 * (endTime - startTime) / 3;
double equityPeak1 = 0.0, equityPeak2 = 0.0, equityPeak3 = 0.0;
double drawdown1 = 0.0, drawdown2 = 0.0, drawdown3 = 0.0;
int trades1 = 0, trades2 = 0, trades3 = 0;
// 这里需要做自定义回测模拟
// 下面是简化表示 - 实际实现需要模拟OrderSend
// 并使用传进来的参数
// 对每个子周期模拟交易
for(int i = 0; i < Bars - 1; i++)
{
datetime barTime = iTime(Symbol(), Period(), i);
double currentEquity = 10000.0; // 初始资金,会被交易修改
// 判断当前K线属于哪个子周期
if(barTime >= startTime && barTime < mid1)
{
// 用param1, param2, param3运行交易逻辑
// 更新equityPeak1, drawdown1, trades1
}
else if(barTime >= mid1 && barTime < mid2)
{
// 中间周期的交易逻辑
}
else if(barTime >= mid2 && barTime <= endTime)
{
// 最后周期的交易逻辑
}
}
// 计算各个周期利润因子的变异系数
double pf1 = (trades1 > 0) ? equityPeak1 / drawdown1 : 0.0;
double pf2 = (trades2 > 0) ? equityPeak2 / drawdown2 : 0.0;
double pf3 = (trades3 > 0) ? equityPeak3 / drawdown3 : 0.0;
double meanPF = (pf1 + pf2 + pf3) / 3.0;
double stdDev = MathSqrt((MathPow(pf1 - meanPF, 2) +
MathPow(pf2 - meanPF, 2) +
MathPow(pf3 - meanPF, 2)) / 3.0);
// 稳健性得分:均值高、方差小
// 最大值为1.0(完美稳定),最小值为0.0
double rs = 0.0;
if(meanPF > 0.0 && stdDev > 0.0)
{
rs = meanPF / (meanPF + stdDev);
}
return rs;
}
//+------------------------------------------------------------------+
```
这个方法为什么不一样
与依赖GA内置的适应度(单一标量)不同,这个方法强制参数组在*同一段历史数据的不同时间分区*上都表现一致。它不算真正意义上的样本外测试,但起到了正则化惩罚的作用。那些利用了2022年6月某个特定异常行情的参数,会在三个周期之间表现出高方差,从而被惩罚。
这个技巧我用了大概两年了。我观察到的现象是,用稳健性得分排在最前面的参数组,在完整历史数据上的净利润通常会稍微低一些——大概低15-20%——但它们的前向测试结果稳定性要高出40-50%。
一个容易被忽视的问题:分笔数据差异
这里有个不太被讨论到的问题。MT4策略测试器默认使用“开盘价”或“控制点”来做回测。除非你明确下载并选择了“每个分笔”(这需要从你的经纪商那里获取完整的分笔历史),否则GA是在压缩数据上做优化的。
我是在读了Robert Pardo的《交易策略评估与优化》(Wiley, 2008)这本书之后开始深挖这个问题的。Pardo提出了一个很有说服力的观点:对于短线策略来说,没有分笔级别精度的优化基本没有意义。但即便对于长线策略,在更低的数据分辨率下,GA的参数敏感性也会急剧增加。
做个简单的测试:对同一个30分钟图策略,分别在“每个分笔”和“开盘价”两种模式下运行完全相同的优化。最优参数组几乎没有重叠。我测过一个EA,两种数据分辨率下的最优参数重合度只有78%。
我的建议是:永远从你经纪商的历史中心下载分笔数据(工具>选项>图表>“使用分笔数据测试”这个选项需要勾上),然后先在一个小参数范围上做一次初步优化,看看适应度地形是不是平滑的。如果GA在分笔数据上代与代之间跳变得很厉害,那你的策略根本还不够稳健去优化。没得商量。
关于推进分析的一个不同做法
标准的推进分析方法:样本内优化,样本外测试,重复。MT4里实现起来的问题是手动操作很繁琐。我用一个自定义EA把推进分析自动化了,直接把优化结果写入CSV文件。
核心逻辑如下:
```mql4
//+------------------------------------------------------------------+
//| 自定义推进分析自动化 |
//| 把优化结果保存到CSV文件以便外部分析 |
//+------------------------------------------------------------------+
void WriteOptimizationResult(int handle, int generation, double fitness,
int param1, int param2, double param3)
{
string fileName = "WalkForward_Results_" + Symbol() + "_" +
IntegerToString(Period()) + ".csv";
int fileHandle = FileOpen(fileName, FILE_READ|FILE_WRITE|FILE_CSV,
",");
if(fileHandle != INVALID_HANDLE)
{
// 如果文件为空,写入表头
if(FileSize(fileHandle) == 0)
{
FileWrite(fileHandle, "Generation", "Fitness",
"Param1", "Param2", "Param3",
"Timestamp");
}
// 移动到文件末尾
FileSeek(fileHandle, 0, SEEK_END);
// 写入数据
FileWrite(fileHandle, generation, fitness,
param1, param2, param3,
TimeToString(TimeCurrent()));
FileClose(fileHandle);
}
}
//+------------------------------------------------------------------+
```
这里真正的价值不在于CSV导出本身。而在于对同一个货币对,拿5家不同经纪商的数据分别跑这个流程。如果GA得出的最优参数在不同经纪商之间差异很大(肯定会,因为分笔数据源不一样),那就说明你的优化对微观结构噪声过于敏感了。
一个反直觉的解决方案
大多数人减少过拟合的方法是简化策略或者减少参数数量。这个思路没问题,但不是唯一的办法。我的做法不是减少参数,而是增加优化目标的个数。
与其只最大化利润因子,我构建了一个复合适应度分数:
Fitness = (ProfitFactor * 0.3) + (SharpeRatio * 0.3) + (RobustnessScore * 0.4)
夏普比率的计算方式是(平均单笔盈利 / 单笔盈利标准差)* sqrt(252)。我是在优化过程中在EA内部手动算这个值的,因为MT4测试器自带的夏普比率是基于日收益率算的,对于日内策略来说相关性没那么大。
前面说的稳健性得分占适应度的40%。这就迫使GA向着又好*又*稳定的方向去演化参数,而不是只在一个时间段特别优秀、在另一个时间段特别糟糕。
换成这种复合适应度之后,我的优化时间增加了大概20%(因为GA需要更多代才能收敛),但前向测试结果明显改善了。有个案例里,6个月前向测试的权益曲线保留了回测表现的82%,而用默认利润因子优化的话只有49%。
参考来源
1. MetaQuotes Software Corp. (2023). *MQL4 Reference - Optimization*. Retrieved from docs.mql4.com/ru/optimization/geneticalg
2. Pardo, R. (2008). *The Evaluation and Optimization of Trading Strategies* (2nd ed.). Wiley Trading.
3. Holland, J. H. (1975). *Adaptation in Natural and Artificial Systems*. University of Michigan Press.
4. Bailey, D. H., & López de Prado, M. (2014). *The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting, and Non-Normality*. Journal of Portfolio Management, 40(5), 94-107.
本文首发于FXEAR.com,原创内容,未经授权禁止转载。
```