如何在MQL5的EA中实现自优化(基础篇)
让EA自己跟着市场改参数
把EA当成靠移动均线看趋势的交易员,它本来能跑,但如果能随市场状态自己调策略,就成了会学习的黑箱。自优化的核心,是交易跑完一轮后,EA根据最新市况重写下一轮的参数,而不是等人手改。 主要好处很直接:MT5上的EA能7×24小时盯着秒级机会,省掉持续人工监督;市场一变,参数自动跟,策略不中断。原文给出的背景数据是,这类思路的示例文章在2025年5月5日发布,浏览量1605,说明关注的人不少但远没烂大街。 坑也得认:最典型的是过度拟合近期数据,换个行情就拉胯;再就是代码复杂了之后CPU吃不消,参数切换时稳定性容易抖。外汇和贵金属杠杆高、跳空频繁,这种自动调参一旦过拟合,实盘回撤可能远超回测。 落地从最朴素的均线交叉开始——趋势市里它从没真正失效过。后面我们要把它改成能自优化的版本,而不是永远用死参数。
「自优化EA的库引入与参数骨架」
要让EA具备自优化能力,先得把底层工具链搭起来。Trade库提供下单、平仓等交易函数,ArrayObj库则用来管理对象动态数组——优化跑出来的多组结果就塞进这种数组里,方便后续按性能排序筛选。 输入参数全部用 input 修饰,交易者能在MT5的EA属性面板直接改,不用碰代码。默认快线周期10、慢线20、SMA方法、收盘价计算、0.1手、止损50点、止盈100点;另有自动优化开关、每5000 ticks触发一次优化、至少攒1000个数据点才允许跑优化。 全局句柄方面,CTrade实例负责实盘交易动作,fastMA_Handle和slowMA_Handle管两条均线的指标句柄,optimizationResults指针指向存优化结果的数组。tickCount用来数tick,决定是否到了重优化的节点。 优化范围写死为常量:快线周期5到50、步长1;慢线周期10到100、步长1。这个范围只是起点,做黄金和欧美时因波动特性不同,倾向根据品种单独调窄或放宽。外汇和贵金属杠杆高、滑点跳空频繁,回测最优不代表实盘能复现,参数需先在策略测试器用历史数据验证。
class="macro">#include <Trade\Trade.mqh> class="macro">#include <Arrays\ArrayObj.mqh> input class="type">int MA_Fast_Period = class="num">10; class=class="str">"cmt">// Fast Moving Average Period input class="type">int MA_Slow_Period = class="num">20; class=class="str">"cmt">// Slow Moving Average Period input ENUM_MA_METHOD MA_Method = MODE_SMA; class=class="str">"cmt">// Moving Average Method input ENUM_APPLIED_PRICE Applied_Price = PRICE_CLOSE; class=class="str">"cmt">// Applied Price input class="type">class="kw">double LotSize = class="num">0.1; class=class="str">"cmt">// Lot Size input class="type">int StopLoss = class="num">50; class=class="str">"cmt">// Stop Loss in points input class="type">int TakeProfit = class="num">100; class=class="str">"cmt">// Take Profit in points class=class="str">"cmt">// Optimization parameters input class="type">bool AutoOptimize = false; class=class="str">"cmt">// Enable Auto Optimization input class="type">int OptimizationPeriod = class="num">5000; class=class="str">"cmt">// Number of ticks between optimizations input class="type">int MinDataPoints = class="num">1000; class=class="str">"cmt">// Minimum number of data points for optimization CTrade trade; class="type">int fastMA_Handle, slowMA_Handle; class="type">class="kw">double fastMA[], slowMA[]; class="type">int tickCount = class="num">0; CArrayObj* optimizationResults; class=class="str">"cmt">// Optimization ranges const class="type">int MA_Fast_Min = class="num">5, MA_Fast_Max = class="num">50, MA_Fast_Step = class="num">1; const class="type">int MA_Slow_Min = class="num">10, MA_Slow_Max = class="num">100, MA_Slow_Step = class="num">1;
◍ EA三个生命周期钩子怎么落地
MT5 的 EA 跑起来靠三个系统回调撑住全程:OnInit 在加载图表瞬间触发,OnDeinit 在移除 EA 或终端关闭时清场,OnTick 则在每个报价跳变时被调用。把这三处写稳,后面双均线交叉的逻辑才有承载。 OnInit 里先建快慢两条 MA 的句柄,顺手 new 一个 CArrayObj 存优化结果。若 iMA 返回 INVALID_HANDLE,直接 return INIT_FAILED,EA 起不来——这能避免图表上挂个死脚本还以为在跑。 OnDeinit 必须 IndicatorRelease 掉两个句柄,并把 optimizationResults 判空后 delete 置 NULL。漏了这步,来回加载 EA 几次就可能看到内存句柄泄漏的报警。 OnTick 开头用 Bars(_Symbol,PERIOD_CURRENT) < MA_Slow_Period 卡掉历史不足的情况;CopyBuffer 只取 0 和 1 两根(最近两根)做交叉判断。若 AutoOptimize 开且 tickCount 累到 OptimizationPeriod,就跳 Optimize() 重置计数。交叉条件写死用 [1] 对 [0]:快线从下穿上为买、从上穿下为卖,止损止盈以 _Point 倍数展开。外汇与贵金属杠杆高,这类均线交叉信号在历史数据不足或震荡市中失效概率偏大,实盘前请在策略测试器跑足样本。
class="type">int OnInit() { class=class="str">"cmt">// Initialize MA handles fastMA_Handle = iMA(_Symbol, PERIOD_CURRENT, MA_Fast_Period, class="num">0, MA_Method, Applied_Price); slowMA_Handle = iMA(_Symbol, PERIOD_CURRENT, MA_Slow_Period, class="num">0, MA_Method, Applied_Price); if(fastMA_Handle == INVALID_HANDLE || slowMA_Handle == INVALID_HANDLE) { Print("Failed to create MA indicators"); class="kw">return INIT_FAILED; } class=class="str">"cmt">// Initialize optimization results array optimizationResults = new CArrayObj(); class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">// Release MA handles IndicatorRelease(fastMA_Handle); IndicatorRelease(slowMA_Handle); class=class="str">"cmt">// Clean up optimization results if(optimizationResults != NULL) { class="kw">delete optimizationResults; optimizationResults = NULL; } } class="type">void OnTick() { class=class="str">"cmt">// Check if we have enough bars to calculate MAs if(Bars(_Symbol, PERIOD_CURRENT) < MA_Slow_Period) class="kw">return; class=class="str">"cmt">// Copy MA values if(CopyBuffer(fastMA_Handle, class="num">0, class="num">0, class="num">2, fastMA) != class="num">2) class="kw">return; if(CopyBuffer(slowMA_Handle, class="num">0, class="num">0, class="num">2, slowMA) != class="num">2) class="kw">return; class=class="str">"cmt">// Auto Optimization if(AutoOptimize && ++tickCount >= OptimizationPeriod) { Optimize(); tickCount = class="num">0; } class=class="str">"cmt">// Trading logic if(fastMA[class="num">1] <= slowMA[class="num">1] && fastMA[class="num">0] > slowMA[class="num">0]) { class=class="str">"cmt">// Open buy position class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); trade.Buy(LotSize, _Symbol, ask, ask - StopLoss * _Point, ask + TakeProfit * _Point); } else if(fastMA[class="num">1] >= slowMA[class="num">1] && fastMA[class="num">0] < slowMA[class="num">0]) { class=class="str">"cmt">// Open sell position class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); trade.Sell(LotSize, _Symbol, bid, bid + StopLoss * _Point, bid - TakeProfit * _Point);
MQL5 中 CSampleClass 的析构与释放
在 MQL5 面向对象写法里,用 new 申请的类实例不会随函数退出自动回收,必须显式调用 delete,否则会造成终端内存占用缓慢堆积。 上面这段收尾代码先对指针做空值判断,非空才执行 delete 并置 nullptr,是防止重复释放导致 MT5 报 'invalid pointer' 错误的标准写法。 实盘脚本里若 CSampleClass 对象在 OnDeinit 未被清理,多次加载 EA 后内存曲线可能每次多出几 KB 到几十 KB,开 MT5 的 'Experts' 日志配合任务管理器即可观察到该现象。
} }
「暴力遍历双均线参数的自优化实现」
EA 自优化的核心落在 Optimize() 上,思路很直接:把快、慢均线周期在给定区间内按步长全部跑一遍,属于典型的暴力搜索。慢周期小于等于快周期的组合会被直接跳过,因为双均线逻辑里慢线必须压在快线之上才有交叉意义。 每组有效参数都交给 TestParameters() 算出一个收益值,封装进 OptimizationResult 对象后塞进 optimizationResults 数组。全部测完再线性扫一遍数组,挑 profit 最大的那组作为最优解。 找到最优解后,代码会把 MA_Fast_Period / MA_Slow_Period 改写成新值,并释放旧指标句柄、用 iMA() 按当前品种和周期重建快慢线句柄。外汇与贵金属市场高杠杆、滑点随机,这种重优化结果在历史样本上表现好,实盘可能明显衰减,上 MT5 跑前先把 MA_Fast_Min/Max/Step 等宏定义核对清楚。 下面这段是 Optimize() 的主体,注意嵌套 for 的步长控制和最后的句柄重绑:
class="type">void Optimize() { Print("Starting optimization..."); optimizationResults.Clear(); class=class="str">"cmt">// Loop through all combinations of MA periods for(class="type">int fastPeriod = MA_Fast_Min; fastPeriod <= MA_Fast_Max; fastPeriod += MA_Fast_Step) { for(class="type">int slowPeriod = MA_Slow_Min; slowPeriod <= MA_Slow_Max; slowPeriod += MA_Slow_Step) { if(slowPeriod <= fastPeriod) class="kw">continue; class=class="str">"cmt">// Slow period should be greater than fast period class="type">class="kw">double profit = TestParameters(fastPeriod, slowPeriod); OptimizationResult* result = new OptimizationResult; result.fastPeriod = fastPeriod; result.slowPeriod = slowPeriod; result.profit = profit; optimizationResults.Add(result); } } class=class="str">"cmt">// Find the best result OptimizationResult* bestResult = NULL; for(class="type">int i = class="num">0; i < optimizationResults.Total(); i++) { OptimizationResult* currentResult = optimizationResults.At(i); if(bestResult == NULL || currentResult.profit > bestResult.profit) { bestResult = currentResult; } } if(bestResult != NULL) { class=class="str">"cmt">// Update the EA parameters MA_Fast_Period = bestResult.fastPeriod; MA_Slow_Period = bestResult.slowPeriod; class=class="str">"cmt">// Update indicator handles IndicatorRelease(fastMA_Handle); IndicatorRelease(slowMA_Handle); fastMA_Handle = iMA(_Symbol, PERIOD_CURRENT, MA_Fast_Period, class="num">0, MA_Method, Applied_Price); slowMA_Handle = iMA(_Symbol, PERIOD_CURRENT, MA_Slow_Period, class="num">0, MA_Method, Applied_Price); Print("Optimization complete. New parameters: Fast MA = ", MA_Fast_Period, ", Slow MA = ", MA_Slow_Period); } else { Print("Optimization failed to find better parameters."); } }
◍ 遍历结果挑出最优解
框架跑完参数遍历后,结果都塞进了 optimizationResults 容器里,下一步就是从中捞出表现最好的那一组。 上面这段逻辑先声明一个 bestResult 指针并置空,随后用 for 循环从索引 0 扫到 optimizationResults.Total()-1,逐次取出当前项 currentResult。 判定规则很直白:若 bestResult 还是空,或者当前项的 profit 大于已记录最优项的 profit,就把 bestResult 指向 currentResult。扫完一轮,bestResult 就是遍历范围内 profit 最高的结果。 在 MT5 里接这段时,注意 profit 字段必须已在 OptimizationResult 结构里正确赋值,否则比较会基于脏数据;外汇与贵金属回测含极高杠杆风险,该 profit 仅反映历史样本表现,实盘可能显著偏离。
OptimizationResult* bestResult = NULL; for(class="type">int i = class="num">0; i < optimizationResults.Total(); i++) { OptimizationResult* currentResult = optimizationResults.At(i); if(bestResult == NULL || currentResult.profit > bestResult.profit) { bestResult = currentResult; } }