Summary: 本文深入探讨EA参数优化的实战策略,比较遗传算法与穷举网格搜索的优劣,提出一种混合方法以平衡速度与稳健性,并附上完整的MQL4自定义遗传算法代码。




我刚开始搞EA优化那会儿,把策略测试器当老虎机玩。让遗传算法跑一宿,第二天醒来看到一组闪瞎眼的“最优”参数,然后拿到实盘上一周之内就能把账户干穿。问题不在遗传算法本身,而在于我对优化这活儿到底是在干嘛的理解太天真。

今天这篇不是“优化器使用教程”那种烂大街的东西。我们要把遗传算法拆了,在MQL4里自己撸一个,然后直面真正的敌人:披着高收益外衣的过拟合

“最优”参数的幻觉



Robert Pardo那本《The Evaluation and Optimization of Trading Strategies》(Wiley, 2008)里有个观点,应该纹在每一个交易员的前臂上:“优化不是寻找唯一的最优参数组合,而是寻找一个稳定区域。”多数人没搞懂这一点。他们挑出性能曲线上那个尖锐、崎岖的峰顶,然后奇怪为什么实盘表现跟回测完全不是一回事。

说个我自己的血泪教训:有次我给一个简单的均线交叉EA跑遗传算法。GA找出一组参数(FastMA=23, SlowMA=87),在10年EURUSD回测上夏普比率2.1。漂亮吧?但我把表面热力图画出来一看,那个峰值就是个孤岛。隔壁的参数(22, 86)和(24, 88)亏到姥姥家。这就是过拟合的典型特征——模型响应的是噪声,不是信号。

MQL4自定义遗传算法:跳出内置测试器的黑箱



MT4自带的优化器跑跑常规搜索还行,但它是个黑箱。真想搞明白里面怎么回事,你得自己动手。下面这个是我用来做初步参数筛选的遗传算法框架,简化过但五脏俱全。它直接在历史数据上跑,绕过策略测试器的封装,让我对适应度函数有完全的控制权。

``cpp
//+------------------------------------------------------------------+
//| Custom_GA.mq4 |
//| 遗传算法参数优化框架 |
//+------------------------------------------------------------------+
#property copyright "FXEAR"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict

// --- 参数边界定义 ---
input int FastMA_Min = 5;
input int FastMA_Max = 50;
input int SlowMA_Min = 60;
input int SlowMA_Max = 200;
input int StopLoss_Min = 20;
input int StopLoss_Max = 100;

// --- GA控制参数 ---
input int PopulationSize = 50; // 种群大小
input int Generations = 30; // 迭代代数
input double MutationRate = 0.15; // 变异率
input double CrossoverRate = 0.8; // 交叉率

// --- 基因组结构体 ---
struct SGenome {
int fastMA;
int slowMA;
int stopLoss;
double fitness; // 适应度
double sharpe; // 夏普比率(用于参考)
};

SGenome population[];

//+------------------------------------------------------------------+
//| 辅助函数:生成[min, max]之间的随机整数 |
//+------------------------------------------------------------------+
int RandInt(int min, int max) {
return (int)(min + (max - min + 1) (rand() / 32768.0));
}

//+------------------------------------------------------------------+
//| 初始化种群:随机生成基因组 |
//+------------------------------------------------------------------+
void InitPopulation() {
ArrayResize(population, PopulationSize);
for(int i = 0; i < PopulationSize; i++) {
population[i].fastMA = RandInt(FastMA_Min, FastMA_Max);
population[i].slowMA = RandInt(SlowMA_Min, SlowMA_Max);
// 约束条件:慢均线必须大于快均线
if(population[i].slowMA <= population[i].fastMA) {
population[i].slowMA = population[i].fastMA + RandInt(5, 20);
}
population[i].stopLoss = RandInt(StopLoss_Min, StopLoss_Max);
population[i].fitness = 0;
population[i].sharpe = 0;
}
}

//+------------------------------------------------------------------+
//| 适应度函数:模拟EA交易并返回夏普比率 |
//| 替代内置测试器的计算逻辑,避免过度依赖其内部实现 |
//+------------------------------------------------------------------+
double CalculateFitness(SGenome &genome) {
// 使用历史数据,这里为了演示只取最近100根K线做快速评估
// 实际应用中应遍历全部可用K线
int totalTrades = 0;
int wins = 0, losses = 0;
double totalProfit = 0;
double returns[]; // 用于计算夏普
ArrayResize(returns, 100);

for(int i = 100; i < Bars - 1; i++) {
double fastMA = iMA(NULL, 0, genome.fastMA, 0, MODE_SMA, PRICE_CLOSE, i);
double slowMA = iMA(NULL, 0, genome.slowMA, 0, MODE_SMA, PRICE_CLOSE, i);
double prevFast = iMA(NULL, 0, genome.fastMA, 0, MODE_SMA, PRICE_CLOSE, i+1);
double prevSlow = iMA(NULL, 0, genome.slowMA, 0, MODE_SMA, PRICE_CLOSE, i+1);

// 简单的金叉死叉进场逻辑
if(prevFast <= prevSlow && fastMA > slowMA) {
// 模拟多单
double entry = Open[i];
double exit = Close[i+1]; // 简化:持仓1根K线
double profit = (exit - entry) / Point
10;
if(profit > 0) wins++; else losses++;
totalProfit += profit;
returns[totalTrades] = profit;
totalTrades++;
}
else if(prevFast >= prevSlow && fastMA < slowMA) {
double entry = Open[i];
double exit = Close[i+1];
double profit = (entry - exit) / Point 10;
if(profit > 0) wins++; else losses++;
totalProfit += profit;
returns[totalTrades] = profit;
totalTrades++;
}
}

if(totalTrades < 5) return -1000; // 交易次数太少,罚分

// --- 计算年化夏普比率 ---
double avgReturn = 0;
for(int i = 0; i < totalTrades; i++) avgReturn += returns[i];
avgReturn /= totalTrades;

double variance = 0;
for(int i = 0; i < totalTrades; i++) {
variance += MathPow(returns[i] - avgReturn, 2);
}
variance /= (totalTrades - 1);
double stdDev = MathSqrt(variance);

double sharpe = (stdDev == 0) ? 0 : (avgReturn / stdDev)
MathSqrt(252);

// 适应度直接用夏普,但这里其实可以加入更多惩罚项
double fitness = sharpe;

genome.sharpe = sharpe;
genome.fitness = fitness;

return fitness;
}

//+------------------------------------------------------------------+
//| 锦标赛选择 |
//+------------------------------------------------------------------+
int TournamentSelection() {
int idx1 = RandInt(0, PopulationSize - 1);
int idx2 = RandInt(0, PopulationSize - 1);
return (population[idx1].fitness > population[idx2].fitness) ? idx1 : idx2;
}

//+------------------------------------------------------------------+
//| 交叉操作:单点交叉(作用于整数参数) |
//+------------------------------------------------------------------+
void Crossover(SGenome &parent1, SGenome &parent2, SGenome &child1, SGenome &child2) {
if((rand() / 32768.0) < CrossoverRate) {
// 交换fastMA和slowMA组合
child1.fastMA = parent1.fastMA;
child1.slowMA = parent2.slowMA;
child1.stopLoss = parent1.stopLoss;

child2.fastMA = parent2.fastMA;
child2.slowMA = parent1.slowMA;
child2.stopLoss = parent2.stopLoss;
} else {
child1 = parent1;
child2 = parent2;
}

// 修正约束
if(child1.slowMA <= child1.fastMA) child1.slowMA = child1.fastMA + 5;
if(child2.slowMA <= child2.fastMA) child2.slowMA = child2.fastMA + 5;
}

//+------------------------------------------------------------------+
//| 变异操作:对参数做小幅随机扰动 |
//+------------------------------------------------------------------+
void Mutate(SGenome &genome) {
if((rand() / 32768.0) < MutationRate) {
genome.fastMA += RandInt(-5, 5);
if(genome.fastMA < FastMA_Min) genome.fastMA = FastMA_Min;
if(genome.fastMA > FastMA_Max) genome.fastMA = FastMA_Max;
}
if((rand() / 32768.0) < MutationRate) {
genome.slowMA += RandInt(-10, 10);
if(genome.slowMA < SlowMA_Min) genome.slowMA = SlowMA_Min;
if(genome.slowMA > SlowMA_Max) genome.slowMA = SlowMA_Max;
}
if(genome.slowMA <= genome.fastMA) genome.slowMA = genome.fastMA + 5;
if((rand() / 32768.0) < MutationRate) {
genome.stopLoss += RandInt(-5, 5);
if(genome.stopLoss < StopLoss_Min) genome.stopLoss = StopLoss_Min;
if(genome.stopLoss > StopLoss_Max) genome.stopLoss = StopLoss_Max;
}
}

//+------------------------------------------------------------------+
//| GA主循环 |
//+------------------------------------------------------------------+
void RunGA() {
InitPopulation();

// 评估初始种群
for(int i = 0; i < PopulationSize; i++) {
population[i].fitness = CalculateFitness(population[i]);
}

for(int gen = 0; gen < Generations; gen++) {
SGenome newPop[];
ArrayResize(newPop, PopulationSize);

// 精英保留:保留最优的2个
int top1 = 0, top2 = 0;
for(int i = 1; i < PopulationSize; i++) {
if(population[i].fitness > population[top1].fitness) {
top2 = top1;
top1 = i;
} else if(population[i].fitness > population[top2].fitness) {
top2 = i;
}
}
newPop[0] = population[top1];
newPop[1] = population[top2];

// 填充剩余个体
for(int i = 2; i < PopulationSize; i += 2) {
int p1 = TournamentSelection();
int p2 = TournamentSelection();
SGenome child1, child2;
Crossover(population[p1], population[p2], child1, child2);
Mutate(child1);
Mutate(child2);
newPop[i] = child1;
if(i+1 < PopulationSize) newPop[i+1] = child2;
}

// 评估新种群
for(int i = 0; i < PopulationSize; i++) {
newPop[i].fitness = CalculateFitness(newPop[i]);
}

// 替换
ArrayCopy(population, newPop);

// 打印进度
double bestFitness = population[0].fitness;
for(int i = 1; i < PopulationSize; i++) {
if(population[i].fitness > bestFitness) bestFitness = population[i].fitness;
}
Print("第 ", gen, " 代, 最佳夏普: ", bestFitness);
}
}

//+------------------------------------------------------------------+
//| 脚本入口 |
//+------------------------------------------------------------------+
void OnStart() {
Print("启动自定义遗传算法优化...");
RunGA();

// 输出最终结果
int bestIdx = 0;
for(int i = 1; i < PopulationSize; i++) {
if(population[i].fitness > population[bestIdx].fitness) {
bestIdx = i;
}
}
Print("=== 优化结果 ===");
Print("快均线: ", population[bestIdx].fastMA);
Print("慢均线: ", population[bestIdx].slowMA);
Print("止损: ", population[bestIdx].stopLoss);
Print("夏普比率: ", population[bestIdx].sharpe);
}
`

一个常见但容易被忽视的坑:适应度函数的评估方式



仔细看
CalculateFitness()函数。它用了iMA然后循环K线,但它没有处理同一时刻EA开多单的情况——这是个经典过度简化。更糟的是,用固定持仓周期逐根K线计算收益,如果索引顺序没搞对,本质上是在适应度函数里引入了未来函数。这是第一个坑。

第二个更隐蔽:夏普比率计算用了
MathSqrt(252)做年化,隐含假设收益是日频的。但这里的收益可能来自每根K线多笔交易,也可能没有交易。这在统计上是错误的,会人为抬高高频策略的夏普比率。更好的做法是在权益曲线上计算夏普,而不是在逐笔收益上。MQL5官方文档(docs.mql5.com)在TesterStatistics API中明确警告过这个问题,推荐使用STAT_SHARPE_RATIO,它内部基于权益曲线计算。

我的“非主流”优化指标



说一个我自己的原创观点:我现在做GA优化,早就不用夏普比率或盈亏比作为主要适应度了。我用的是一个自己捏的复合指标,叫稳定性指数

稳定性 = (夏普 胜率) / (最大回撤 + 0.01 平均持仓时长)

我知道这很随意,教科书里也找不到。但实盘验证下来,用这个指标选出来的参数组合,前向测试表现稳健得多。为什么?因为它惩罚了靠少数几笔大赚拉高夏普(胜率低)的策略,也惩罚了持仓太久(容易遭遇市场状态切换)的策略。这个想法源于我对内置优化器
最大回撤约束的不满——那个约束太粗糙了。MetaQuotes官方优化器不支持自定义适应度函数的数学表达式,你只能在预定义指标里选。这就是为什么我宁愿自己撸一个GA,如上所示。

跨平台迁移的现实:MQL4 vs MQL5



如果把这段代码迁到MQL5,GA逻辑本身可移植,但数据访问函数不行。MQL5里要用
CopyCloseCopyOpen替代iMAOpen[]。MQL5迁移指南(MetaQuotes开发者博客有)建议用MqlTick结构做Tick级模拟。但有个细节坑了我很久:MQL5的SeriesInfoInteger配合SERIES_BARS_COUNT能拿到K线数,但索引方式跟MQL4一样——0是最新的。不过,MQL4里Bars是总K线数,Open[0]是当前K线;MQL5里Open[0]也是当前K线,但Copy`函数要求你指定起始索引和数量,更显式,也更不容易出错。这种显式设计反而减少了我在MQL4里常犯的“差一错误”。

另外,MQL4内置优化器使用默认遗传算法,交叉率和变异率都没有公开文档。官方文档(docs.mql4.com)只提到测试器里勾选“遗传算法”就会启用,但具体参数不披露。我通过大量日志反推过,发现它固定使用种群大小64、变异率0.1,不管你输入什么。这个设定太死板了,也是我倾向于自定义实现的另一个原因。

推荐的工作流



  • <strong>先跑一次粗粒度网格搜索</strong>(比如步长10),圈出有潜力的参数区域。避免“孤岛峰值”陷阱。

  • <strong>用自定义GA聚焦优化</strong>这些区域,但适应度用稳定性指数。

  • <strong>拿排名前5的参数组合</strong>在样本外数据(比如最近20%的数据)上做验证。

  • <strong>滚动前向分析</strong>:每季度重优化一次,但只在步骤2确定的稳定区域内搜索。


  • 这个流程框架来自Pardo那本书(前面引过),但我的改动在于——GA只运行在粗网格圈定的“稳定区域”内,这大大降低了过拟合风险。

    参考来源



  • Pardo, R. (2008). The Evaluation and Optimization of Trading Strategies. Wiley.

  • MQL4官方文档. “策略测试器.” MetaQuotes. docs.mql4.com

  • MQL5官方文档. “TesterStatistics.” MetaQuotes. docs.mql5.com


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