多数交易者对待优化就像对待老虎机——拉下杠杆,等待闪亮的结果,然后在实盘交易中迅速亏钱。问题不在于优化本身,而在于围绕它的盲目跟风心态。过去五年我一直在深入研究MQL4回测,可以百分之百确定地说:MT4默认的策略测试器既是礼物也是诅咒。它赋予你力量,同时也递给你足够上吊的绳子。
这篇文章不是另一篇泛泛的“如何优化”指南。我们将深入剖析自适应推进分析方法,实现一个自定义的遗传算法片段以解决MT4内置优化器的一个明显缺陷,并讨论为什么你的优化结果很可能在撒谎。
“最优”参数集的巨大欺骗
让我们直面房间里的大象:过度拟合。MT4测试器默认使用暴力搜索或遗传算法,根据你选择的准则(通常是利润因子或净利润)找到最优参数。问题在于,这个过程是在拟合历史时期的噪声。Robert Pardo在其开创性著作《交易策略的评估与优化》(Wiley, 2008)中指出,策略的验证比优化更重要。他提出,样本外测试是稳健性的唯一真正衡量标准。然而,我们中有多少人真正运行过适当的推进分析?
典型的工作流程是:
缺失的环节是稳定性。在整个数据集上表现优异但在各子时期表现剧烈波动的参数组,是一颗定时炸弹。优化的目标不应仅仅是最大化利润,而应是最大化一致性。
自适应推进逻辑
推进分析是黄金标准。其工作原理如下:
我提出的“自适应”部分不仅仅在于滚动窗口,而在于对优化目标进行加权。在传统的推进分析中,样本内时期被视为一个整体。但市场在变化。在样本内时期前半段有效的参数,在后半段可能是有害的。
这是我的独家视角:不要针对总净利润进行优化,而是针对样本内时期的滚动夏普比率进行优化,但如果样本内时期后半段的业绩显著差于前半段,则对策略进行惩罚。这充当了一个实时的“变点”检测器。它迫使优化过程找到不仅在历史上盈利,而且在近期市场状态下稳定的参数。
MQL4遗传算法代码片段(及其致命缺陷)
MT4内置的遗传算法不错,但它有一个巨大的盲点:它不允许自定义播种或多样性保持。当参数空间较大时,它很容易快速收敛到局部最优解。
让我们构建一个引入自适应变异的自定义遗传算法。内置遗传算法使用固定的变异率。我们的版本将在种群的平均适应度停滞超过5代时增加变异率。这是从进化策略文献中借鉴的技术,特别是Schwefel和Bäck的工作。
以下是一个简化的、可编译的MQL4脚本,演示了针对两个变量参数集(
MAPeriod 和 StopLoss)的这种自适应变异逻辑。``
mql4
//+------------------------------------------------------------------+
//| AdaptiveGA_Example.mq4 |
//| (C) FXEAR.com - 概念验证 |
//| |
//| 这是一个独立的脚本,用于演示自适应变异。 |
//| 在实际的EA中,你需要将此逻辑集成到OnStart()或自定义优化例程中。 |
//+------------------------------------------------------------------+
#property strict
// 种群结构
struct SIndividual {
int MA_Period;
int SL_Points;
double Fitness; // 例如:夏普比率或利润因子
};
// 遗传算法输入参数
input int MaxGenerations = 50;
input int PopulationSize = 20;
input int MinMAPeriod = 5;
input int MaxMAPeriod = 100;
input int MinSL = 20;
input int MaxSL = 200;
// 全局数组
SIndividual Population[];
double FitnessHistory[];
int StagnationCounter = 0;
double PrevAvgFitness = 0.0;
//+------------------------------------------------------------------+
//| 脚本程序启动函数 |
//+------------------------------------------------------------------+
void OnStart() {
// 1. 初始化种群
ArrayResize(Population, PopulationSize);
for (int i = 0; i < PopulationSize; i++) {
Population[i].MA_Period = RandInt(MinMAPeriod, MaxMAPeriod);
Population[i].SL_Points = RandInt(MinSL, MaxSL);
Population[i].Fitness = CalculateFitness(Population[i].MA_Period, Population[i].SL_Points);
}
// 2. 主遗传算法循环
for (int gen = 0; gen < MaxGenerations; gen++) {
// 按适应度排序(降序)
SortPopulation();
// 计算平均适应度以检测停滞
double avgFit = GetAverageFitness();
FitnessHistory[gen] = avgFit;
if (gen > 0) {
if (MathAbs(avgFit - PrevAvgFitness) < 0.0001) {
StagnationCounter++;
} else {
StagnationCounter = 0;
}
}
PrevAvgFitness = avgFit;
// 自适应变异逻辑
double MutationRate = 0.10; // 基础10%
if (StagnationCounter >= 5) {
MutationRate = 0.35; // 增加到35%以跳出局部最优
Print("第 ", gen, " 代: 检测到停滞。变异率提高到35%。");
}
// 交叉和变异(精英策略:保留前2名)
int eliteCount = 2;
SIndividual NewPop[];
ArrayResize(NewPop, PopulationSize);
// 保留精英
for (int e = 0; e < eliteCount; e++) {
NewPop[e] = Population[e];
}
// 填充剩余部分
for (int j = eliteCount; j < PopulationSize; j++) {
// 锦标赛选择
int parent1 = TournamentSelect();
int parent2 = TournamentSelect();
// 交叉(均匀交叉)
NewPop[j].MA_Period = (MathRand() % 2 == 0) ? Population[parent1].MA_Period : Population[parent2].MA_Period;
NewPop[j].SL_Points = (MathRand() % 2 == 0) ? Population[parent1].SL_Points : Population[parent2].SL_Points;
// 自适应变异
if (MathRand() / 32767.0 < MutationRate) {
// 变异MA周期
int delta = RandInt(-10, 10);
NewPop[j].MA_Period = MathMax(MinMAPeriod, MathMin(MaxMAPeriod, NewPop[j].MA_Period + delta));
}
if (MathRand() / 32767.0 < MutationRate) {
// 变异止损
int delta = RandInt(-20, 20);
NewPop[j].SL_Points = MathMax(MinSL, MathMin(MaxSL, NewPop[j].SL_Points + delta));
}
// 评估
NewPop[j].Fitness = CalculateFitness(NewPop[j].MA_Period, NewPop[j].SL_Points);
}
// 替换种群
ArrayCopy(Population, NewPop);
// 输出最佳结果
Print("第 ", gen, " 代 最佳适应度: ", Population[0].Fitness, " MA: ", Population[0].MA_Period, " SL: ", Population[0].SL_Points);
}
}
//+------------------------------------------------------------------+
//| 辅助函数:随机整数 |
//+------------------------------------------------------------------+
int RandInt(int minVal, int maxVal) {
return (minVal + (MathRand() % (maxVal - minVal + 1)));
}
//+------------------------------------------------------------------+
//| 模拟适应度函数(替换为你的EA逻辑) |
//| 实际上,你需要在这里针对特定参数运行回测。 |
//+------------------------------------------------------------------+
double CalculateFitness(int maPeriod, int slPoints) {
// 这是一个占位符。在实际场景中,这里会调用回测。
// 我们创建一个虚构的“适应度地形”,峰值位于 maPeriod=50, slPoints=80 附近。
double fit = 10.0 - MathAbs(maPeriod - 50) 0.1 - MathAbs(slPoints - 80) 0.05;
// 加入一些噪声
fit += (MathRand() / 32767.0) * 2.0;
return MathMax(0.1, fit);
}
//+------------------------------------------------------------------+
//| 排序种群(为简化使用冒泡排序) |
//+------------------------------------------------------------------+
void SortPopulation() {
for (int i = 0; i < PopulationSize - 1; i++) {
for (int j = 0; j < PopulationSize - i - 1; j++) {
if (Population[j].Fitness < Population[j+1].Fitness) {
SIndividual temp = Population[j];
Population[j] = Population[j+1];
Population[j+1] = temp;
}
}
}
}
//+------------------------------------------------------------------+
//| 锦标赛选择 |
//+------------------------------------------------------------------+
int TournamentSelect() {
int idx1 = RandInt(0, PopulationSize - 1);
int idx2 = RandInt(0, PopulationSize - 1);
return (Population[idx1].Fitness > Population[idx2].Fitness) ? idx1 : idx2;
}
//+------------------------------------------------------------------+
//| 计算平均适应度 |
//+------------------------------------------------------------------+
double GetAverageFitness() {
double sum = 0.0;
for (int i = 0; i < PopulationSize; i++) {
sum += Population[i].Fitness;
}
return sum / PopulationSize;
}
//+------------------------------------------------------------------+
`
细节之处: 注意 CalculateFitness 函数。在真实的EA中,这会执行对样本内数据的完整回测。这计算量很大。使其高效工作的“诀窍”是使用离散事件模拟,而不是逐跳动模拟。大多数人直接使用内置测试器,但这很慢。我建议在EA内部编写一个自定义回测引擎,从数组中读取OHLCV数据。这样更快,并且让你对执行逻辑有更多控制。
优化中
OrderSend 的常见陷阱
这里有一个典型的“陷阱”,它会毁掉你的优化:OrderSend 错误。在优化期间,测试器会运行数千次迭代。如果你的代码对某些参数返回 -1(失败),测试器通常会将该次运行记为利润为零。但如果它有时失败呢?MT4的 OrderSend 函数因返回 -1 而臭名昭著,原因包括“常见错误”,如 130(无效止损)或 138(重新报价)。测试器可能在前几根K线通过 OrderSend,但在最后一根K线失败,从而扭曲你的结果。
我的规则: 始终将你的 OrderSend 包裹在带有重试机制的循环中。但为了优化目的,你需要一个预检查。在发送订单之前,计算止损和止盈水平,并验证它们是否在当前 MarketInfo(Symbol(), MODE_STOPLEVEL) 范围内。如果不在,则跳过该交易或调整它们。不要依赖错误处理程序来拯救你,因为测试器只会将其记为亏损,而你不会知道原因。
`mql4
// 带预验证的安全OrderSend包装函数
bool SafeOrderSend(int cmd, double price, double sl, double tp) {
int stopLevel = (int)MarketInfo(Symbol(), MODE_STOPLEVEL);
if (cmd == OP_BUY) {
if (sl != 0 && price - sl < stopLevel Point) return false;
if (tp != 0 && tp - price < stopLevel Point) return false;
} else if (cmd == OP_SELL) {
if (sl != 0 && sl - price < stopLevel Point) return false;
if (tp != 0 && price - tp < stopLevel Point) return false;
}
// 如果验证通过,进行OrderSend并重试循环(3次尝试)
for(int attempt = 0; attempt < 3; attempt++) {
int ticket = OrderSend(Symbol(), cmd, Lots, price, 3, sl, tp, "EA_Opt", Magic, 0, clrNONE);
if(ticket > 0) return true;
Sleep(100); // 等待市场更新
RefreshRates();
}
return false;
}
``调试优化运行
内置优化器会给你一个漂亮的图表,但它不会给你一致性指标。我总是添加一个名为“效率比率”的自定义指标,这借鉴自Kaufman的自适应移动平均线。在优化背景下,我计算权益曲线斜率的风险回报比。净利润高但权益曲线波动剧烈的策略是危险的。
$$
\text{优化评分} = \text{夏普比率} \times \left(1 - \frac{\text{最大回撤}}{\text{净利润}}\right)
$$
这有效地惩罚了回撤。
参考来源
这种方法不能保证你会得到一个盈利的策略,但能保证你不会自欺欺人。优化是一面镜子;它反映的是你的假设。确保你看到的是清晰的反射,而不是哈哈镜。
本文首发于FXEAR.com,原创内容,未经授权禁止转载。