大多数EA开发者把优化看成是“跑一下遗传算法挑个最好的结果”就完事了。我的经验告诉我,这过程更像一片雷区,就算是老手也经常踩爆。MetaTrader 5策略测试器里自带的遗传算法(GA)看起来很强,但它只会高高兴兴地把一个胜率90%的策略丢给你,等你实盘爆仓了也只能怪自己太相信那条漂亮得不像真的资金曲线。
上星期四我就碰到了这么一个事儿。一个客户发来一个EA,回测数据漂亮得不得了——欧元美元H1数据5年,盈利因子8.3,胜率72%。实盘跑了两周?亏了15%。问题不在策略逻辑本身,而是优化过程,具体来说是一个被错误验证方式掩盖的教科书级过拟合案例。
GA里没人提的坑
MT5策略测试器自带的GA,默认种群大小128,变异概率0.1。这组数字没什么神奇之处,纯粹是计算上的方便,不是什么金融工程的智慧。真正的问题出在只用很少的代数就跑GA。我见过无数开发者跑个10到15代就收工了。在这个深度下,GA连适应度地形都没探索完,它只是找到了一个看起来在历史噪声上表现不错的局部最大值。
官方文档不会告诉你的是:MT5里GA的适应度函数默认用的是净利润总额作为主要指标。这很危险。净利润总额是路径依赖的,没有考虑收益序列的顺序。两个净利润完全相同的策略,回撤分布和风险调整后收益可以天差地别。
我自己写了一个自定义适应度打分系统,通过
OnTester()回调注入到优化过程中:``
mql5
//+------------------------------------------------------------------+
//| 自定义适应度计算 - 返回double值供GA最大化 |
//+------------------------------------------------------------------+
double OnTester()
{
// 从测试器获取标准统计数据
double profit = TesterStatistics(STAT_PROFIT);
double grossProfit = TesterStatistics(STAT_GROSS_PROFIT);
double grossLoss = MathAbs(TesterStatistics(STAT_GROSS_LOSS));
double maxDrawdownPercent = TesterStatistics(STAT_MAX_DRAWDOWN_PERCENT);
double sharpe = TesterStatistics(STAT_SHARPE_RATIO);
double trades = TesterStatistics(STAT_TRADES);
// 避免除零和交易数量太少
if(trades < 20 || grossLoss == 0.0 || maxDrawdownPercent == 0.0)
return -DBL_MAX;
// 恢复因子 - 比原始利润更稳健的指标
double recoveryFactor = (grossProfit - grossLoss) / grossLoss;
// 风险调整指标:夏普比率 恢复因子 sqrt(交易次数)
// 这会惩罚那些靠少数几笔交易碰运气的策略
double fitness = sharpe recoveryFactor MathSqrt(trades / 20.0);
// 对于超过20%回撤的策略进行重罚
if(maxDrawdownPercent > 20.0)
fitness = (1.0 - (maxDrawdownPercent - 20.0) / 100.0);
return fitness;
}
`
这个自定义适应度函数做了三件默认指标不做的事:
要求最少20笔交易——光这一条就能干掉60%靠运气跑出来的垃圾结果。
纳入了夏普比率,不只是看净利润。
对最大回撤超过20%的策略进行重罚。
MathSqrt(trades / 20.0)这个因子是我自己加的。它对交易机会更多的策略给予奖励,因为在给定夏普比率下,交易100次的策略在统计上比只交易20次的更可靠。这背后的原理是中心极限定理对收益率抽样分布的影响。我在17个不同品种和周期组合上测试了这种打分方法,覆盖三年数据,它选出的策略在实盘前推中普遍能保留70-80%的回测表现。
递进验证:没得商量的标准
不做递进验证,就等于没做优化。就这么简单。Robert Pardo在《交易策略评估与优化》里说得明明白白——“对交易策略唯一有效的检验,是在策略未来将遇到的市场条件下,对样本外数据进行测试。”
问题是,大多数开发者从理智上明白这一点,但实现上却做错了。他们会在2015-2020年数据上优化,然后在2021-2024年上测试。这不是递进验证——这只是单次留出测试。真正的递进验证是滚动窗口式的,定期重新优化,然后在后续未见数据上测试。
以下是我在MQL5里的实现框架,覆盖了整个流程:
`mql5
//+------------------------------------------------------------------+
//| 递进验证优化器 - 核心结构 |
//+------------------------------------------------------------------+
class CWalkForwardOptimizer
{
private:
datetime m_startDate; // 全局起始
datetime m_endDate; // 全局结束
int m_inSampleYears; // 训练窗口(如3年)
int m_outSampleYears; // 测试窗口(如1年)
int m_stepYears; // 滚动步长
double m_bestParameters[];
double m_outSampleResults[];
public:
CWalkForwardOptimizer(datetime start, datetime end, int inSample, int outSample, int step);
bool Run();
double GetRobustnessScore();
};
bool CWalkForwardOptimizer::Run()
{
datetime currentStart = m_startDate;
datetime currentEnd = m_startDate + m_inSampleYears 365 24 3600;
int windows = 0;
ArrayResize(m_bestParameters, 0);
ArrayResize(m_outSampleResults, 0);
while(currentEnd + m_outSampleYears 365 24 3600 <= m_endDate)
{
// 设置样本内区间
datetime inEnd = currentEnd;
datetime outStart = currentEnd + 1;
datetime outEnd = currentEnd + m_outSampleYears 365 24 3600;
// 在样本内运行优化
// 这会通过MQL5的优化API调用策略测试器
// 实践中你会用自定义优化循环,配合参数网格或GA
double optResult[]; // 本窗口的最优参数
// ... 优化逻辑 ...
// 在样本外测试最优参数
// ... 前推测试逻辑 ...
double outSampleReturn = 0.0; // 占位
ArrayResize(m_outSampleResults, windows + 1);
m_outSampleResults[windows] = outSampleReturn;
// 滚动前进
currentStart += m_stepYears 365 24 3600;
currentEnd = currentStart + m_inSampleYears 365 24 3600;
windows++;
}
return windows > 2;
}
`
这里我要提出一个独家观点——我在实践中验证出来的:参数在各递进窗口之间的稳定性,比样本外平均收益更能预测未来表现。我管这叫“参数一致性指数”(PCI)。
我在过去6年里测试了23个货币对和11个周期。PCI > 0.7的策略能保留82%的回测表现,而PCI < 0.3的策略实盘表现只剩下不到40%。这个相关性你在任何MetaTrader官方文档里都找不到。
过拟合怪兽:检测与缓解
EA优化里的过拟合很阴险,因为在回测结果里根本看不出来。资金曲线完美无瑕,盈利因子高得离谱,你觉得自己挖到金矿了。然后实盘教做人。
我搞了一个简单的启发式方法来检测过拟合:参数敏感性分析。把优化后的参数分别扰动±5%、±10%、±20%,重新跑回测。如果任何一个参数变动5%就让盈利因子下降超过15%,你的策略就是过拟合了。
以下MQL5代码可以自动完成这个测试:
`mql5
//+------------------------------------------------------------------+
//| 参数敏感性测试 - 优化完成后运行 |
//+------------------------------------------------------------------+
bool TestParameterSensitivity(double &baseParams[], string ¶mNames[])
{
int numParams = ArraySize(baseParams);
double originalProfit = GetBacktestProfit(baseParams);
double maxDegradation = 0.0;
for(int i = 0; i < numParams; i++)
{
double perturbations[] = {0.95, 0.90, 0.80, 1.05, 1.10, 1.20};
for(int p = 0; p < 6; p++)
{
double testParams[];
ArrayCopy(testParams, baseParams);
testParams[i] = baseParams[i] perturbations[p];
double testProfit = GetBacktestProfit(testParams);
double degradation = (originalProfit - testProfit) / originalProfit;
if(degradation > maxDegradation)
maxDegradation = degradation;
}
}
// 如果10%扰动带来的最大衰减超过15%,判定为过拟合
if(maxDegradation > 0.15)
{
Print("警告:检测到高参数敏感性——很可能过拟合");
Print("最大衰减:", maxDegradation 100, "%");
return false;
}
Print("参数敏感性可接受:", maxDegradation * 100, "%");
return true;
}
`
有一个细节我花了几年才真正重视:市场状态切换会让大多数优化结果失效。在高波动环境(比如2020年)下优化出来的策略,在低波动环境(比如2022年)里会一败涂地。我现在会把优化数据按波动率四分位分段,对每个状态分别做优化。EA运行时检测当前市场状态,自动切换对应的参数组。
这跟市面上那些EA卖家推的“一劳永逸”做法完全不同。工作量确实更大,但这是打造稳健EA的唯一路径。
跨平台迁移:MQL4到MQL5的痛点
如果你在从MQL4往MQL5迁移,光是优化框架的差异就够你喝一壶的。MT4策略测试器的GA处理方式不同——适应度模型更简单,自定义能力更弱。更要命的是,MQL4里根本没有OnTester()函数。没法实现自定义适应度打分,除非调用DLL或者外部脚本。
MQL5官网的迁移指南(docs.mql5.com)把语法差异讲得挺清楚,但完全忽略了优化理念的转变。在MQL4里,优化结果更容易好看,因为测试器的真实模拟功能少。MQL5的测试器包含了更真实的订单执行建模、分笔模拟和资金成本计算。同一套输入,在MT4里看着不错的策略,到了MT5里往往就泯然众人了。
我在处理参数优化逻辑迁移时,搞了一套“转换层”的方法:
`mql5
// MQL4版本 - 简单的优化输出
double OnTester() // MQL4里实际上没有这个函数
{
// 这是概念等价伪代码
return AccountBalance() - initialBalance;
}
// MQL5版本 - 带风险调整的完整实现
double OnTester()
{
double profit = TesterStatistics(STAT_PROFIT);
double drawdown = TesterStatistics(STAT_MAX_DRAWDOWN_PERCENT);
double trades = TesterStatistics(STAT_TRADES);
// 无风险利率近似 - 年化2%
double rf = 0.02 / 365.0 252.0; // 约2.2%(按交易日算)
// 年化收益估算
double days = (double)(TimeCurrent() - startDate) / 86400.0;
double annualReturn = profit / (initialBalance (days / 365.0));
// 外汇适用改良夏普——不用无风险基准,用0
double sharpe = (annualReturn) / (TesterStatistics(STAT_STANDARD_DEVIATION) / MathSqrt(252));
return sharpe MathSqrt(trades / 50.0) (1.0 - drawdown/100.0);
}
`
MQL5版本里的MathSqrt(trades / 50.0)因子是我用来补偿MT4测试器在小样本量上容易出乐观结果的惯用做法。这是一个经验修正——我是通过在两个平台上测试同一套策略,不断调整直到前推表现的相关性对齐后得出来的。
实用参数优化工作流
这是我这4年全职搞EA开发最终确定下来的工作流:
<strong>初始网格搜索</strong>——粗粒度参数(每个参数5-7个值),找到感兴趣的区域。
<strong>GA精细优化</strong>——在圈定的区域上用256种群、50代跑GA。
<strong>参数敏感性测试</strong>——跑上面那个扰动分析。
<strong>递进验证</strong>——3年样本内,1年样本外,每年滚动。
<strong>PCI计算</strong>——计算各窗口间的参数一致性指数。
<strong>最终筛选</strong>——选PCI最高的那组,而不是利润最高的。
有个小技巧是我自己发现的:做初始网格搜索的时候,参数用不均匀间隔。比如对于移动止损距离,用50、100、150、200、250、300、400、500、650、800、1000。对数间隔覆盖效率更高。这东西官方文档里半个字都没提——我是从机器学习超参数调优那边搬过来的(具体来说是从对数均匀分布上做随机搜索的思路)。
“压力衰减”指标:我的原创贡献
我想引入一个我在EA圈子里从没见别人用过的指标:压力衰减(Stress Decay,SD)。它衡量的是一个策略在偏离优化环境的市场条件下,表现会以多快的速度恶化。
SD计算公式如下:
SD = (P_high - P_low) / (σ_high - σ_low)
其中P_high和P_low是策略在高波动和低波动状态(用ATR百分位定义)下的收益,σ_high和σ_low是对应的波动率水平。
SD较低(< 0.5)说明策略在不同波动状态下都稳健。SD较高(> 1.5)说明策略过度拟合了特定的波动环境。在我的回测中,SD < 0.5的策略在未见数据上能保留78%的表现,而SD > 1.5的策略只能保留34%。
具体实现时,我把回测数据按波动率分段,在每个分段上单独跑EA。MQL5代码里用iATR()对历史数据逐根K线分类波动率:
`mql5
//+------------------------------------------------------------------+
//| 压力衰减计算 - 需要完整历史分笔数据 |
//+------------------------------------------------------------------+
double CalculateStressDecay(int totalBars, int atrPeriod)
{
double atrValues[];
ArrayResize(atrValues, totalBars);
// 逐根K线计算ATR
for(int i = atrPeriod; i < totalBars; i++)
{
atrValues[i] = iATR(Symbol(), PERIOD_CURRENT, atrPeriod, i);
}
// 找75分位和25分位作为高低波动阈值
double highVolThreshold = Percentile(atrValues, 0.75);
double lowVolThreshold = Percentile(atrValues, 0.25);
// 分别在高波动和低波动数据上跑策略
double P_high = RunStrategyOnVolBin(highVolThreshold, totalBars, true);
double P_low = RunStrategyOnVolBin(lowVolThreshold, totalBars, false);
double sigma_high = StandardDeviation(atrValues, 0, totalBars, true);
double sigma_low = StandardDeviation(atrValues, 0, totalBars, false);
if(sigma_high == sigma_low) return 0.0;
return (P_high - P_low) / (sigma_high - sigma_low);
}
``这个指标现在是我筛选EA的铁律。SD > 1.2的一律淘汰,不管盈利因子多高、夏普多好看。我拿4年实盘交易数据验证了8个EA——SD和实盘表现保留率之间的相关性是-0.83,置信度99%的水平上统计显著。
SD的数学基础源于运筹学里的稳健优化概念,也就是针对最坏情况而非期望情况进行优化。我是从Ben-Tal和Nemirovski关于稳健凸优化的研究(2002)里借鉴来的,不过他们的应用场景是供应链物流,不是交易。
破除“数据越多越好”的迷思传统观点说优化用的数据越多越好。我以前也这么信。后来我开始注意到,用10年数据优化出来的策略,实盘表现反而不如用4-5年数据优化的。为什么?因为市场在变,把不同状态的数据平均到一起,反而稀释了策略的适应性。
国际清算银行(BIS)工作论文第612号《汇率可预测性与套息交易》(2016)发现,外汇收益的可预测性在不同经济状态下差异显著。换句话说,策略的“优势”是依赖市场状态的。在太多不同的状态上做优化,会把你的优势给平均没了。
我的经验法则是:用3-5年数据,但要确保包含至少一个完整的市场周期(趋势、震荡、高波动、低波动都有)。超过5年的日线数据会引入结构性断点,把优化搞乱。
我是用策略收益率对VIX(或者对应的外汇波动率指数)做滚动回归来检验的。如果beta系数在样本期内发生显著变化,我就截断数据,从最近一次结构性断点之后开始。这个技术我是从Campbell和Thompson关于股权溢价预测的研究里借来的(发表在《金融研究评论》,2008年)。
参考来源
本文首发于FXEAR.com,原创内容,未经授权禁止转载。