📘

在 MT5 里跑通 EA 的测试与优化

◍ 在 MT5 里跑通 EA 的测试与优化

MetaTrader 5 的策略测试器支持对 MQL5 编写的 EA 进行历史回测与参数优化,这是验证交易逻辑是否站得住脚的第一道关。2013 年 9 月发布的测试指南中,单篇文档在社区获得 12 320 次查看、11 条讨论,说明这套流程长期被实盘交易者反复翻阅。 打开 MT5 的「策略测试器」面板,选好 EA、品种与历史周期后,先跑单次回测确认无报错,再切到优化模式让平台遍历参数组合。外汇与贵金属杠杆高、滑点真实存在,回测优秀不代表实盘必然复制,只能说该逻辑在历史样本上倾向有正期望。 别把正态当圣经:优化结果常落在参数平原的边缘,过度拟合的曲线看着漂亮,换一组行情就可能塌。先手动跑通、再小步调参,比直接丢给遗传算法省心。

EA 盈利目标为什么难落地

写 EA 的人大多有体会:让程序跑出设想中的优异盈利,往往比写逻辑本身更耗精力。 问题通常不在下单函数,而在测试与优化环节缺了系统步骤——参数空间没扫全、过拟合没排查,回测曲线好看实盘就崩。 本文聚焦的,正是把 EA 从「能跑」推到「可能稳定盈利」必须补上的几步关键动作,开 MT5 后你可直接对照自己的工程查漏。

「MT5里逐条清掉编译与运行错误的实战路径」

在 MT5 的 MetaEditor 里按 F7 编译后,工具箱 Error 选项卡会一次性抛出错误与警告的统计,比如一份 354 行的 EA 初稿曾出现 38 个错误加 1 个警告。选项卡里四块信息最关键:错误描述、出错文件名、行号与列号、错误总数,靠 CTRL+G 或右键错误选 Go to line 能直接跳到对应代码位置。 第一批典型坑是类型不匹配:第 16 列第 20 列报 'truncation of constant value',根因是把 Lot 声明成 int 却用 0.1 初始化,改成 double 即可消除。第 31 行报 'STP - unexpected token',实际是第 30 行末尾漏了分号,这种连锁报错必须往上翻一行查。 全局作用域报错也常见,例如第 100 行提示 'if - expressions are not allowed on a global scope',跳过去发现 if 只有右括号没有左括号,补上后 100、107、121 等多处错误一同消失。逐步修而不是批量修,是因为前面的错常掩盖后面的错。 数组与函数名是重灾区:第 56 行 'cciVal1 - array is required' 是因为声明写成 double cciVal1 漏了方括号,补成 cciVal1[] 后连带错误骤减;第 103 行 'GetLastError - undeclared identifier' 和 292 行 'PositionsTotal - undeclared identifier' 都是函数调用忘加空圆括号 ()。第 159 行 '=- l-value required' 则是 if 里把 == 写成 = 的赋值事故。 编译干净还不算完,调试阶段可能爆 'Array out of range'。有段代码用 CopyBuffer 只复制了 3 条记录(Bar 0/1/2),却在第 172 行读 cciVal1[3],越界直接崩。把复制数量提到 5,就能正常取 cciVal1[0]~[4],EA 跑起来不再报错。外汇与贵金属 EA 编写涉及真金白银,这类运行时越界在高波动品种上可能直接跳过信号,风险偏高,建议每次改完都单步调试验证。

MQL5 / C++
class=class="str">"cmt">//--- 我们是否有足够用于处理的柱数
  class="type">int Mybars=Bars(_Symbol,_Period);
  if(Mybars<class="num">60) class=class="str">"cmt">// 如果总柱数少于60
    {
      Alert("我们有的柱数不足60, EA 将要退出!!");
      class="kw">return;
    }
class=class="str">"cmt">//--- 其他参数
class="type">int maHandle;              class=class="str">"cmt">// 我们移动平均指标的句柄
class="type">int cciHandle1,cciHandle2; class=class="str">"cmt">// 我们CCI指标的句柄
class="type">class="kw">double maVal[];            class=class="str">"cmt">// 用于保持每个柱的移动平均值的动态数组
class="type">class="kw">double cciVal1,cciVal2[];  class=class="str">"cmt">// 用于保持每个柱CCI指标值的动态数组
class="type">class="kw">double p1_close,p2_close;  class=class="str">"cmt">// 用于分别保存柱1和柱2收盘价的变量
class=class="str">"cmt">//--- 其他参数
class="type">int maHandle;              class=class="str">"cmt">// 我们移动平均指标的句柄

◍ 缓冲区复制与持仓判定的代码骨架

这段片段把双 CCI 加 MA 的 EA 里最容易被忽视的底层变量和取数逻辑摊开了。cciHandle1、cciHandle2 是两个周期不同的 CCI 句柄,maVal 存移动平均,cciVal1/cciVal2 分别接两个 CCI 的输出,p1_close、p2_close 抓相邻两根 K 线收盘价做比对。 实时报价别用老式 Bid/Ask 全局变量,直接用 SymbolInfoTick(_Symbol, latest_price) 把最新 tick 塞进 MqlTick 结构;若返回 false,Alert 弹错误码并 return,避免脏数据继续跑。外汇和贵金属报价跳变快,这种容错少了就可能在高点误判。 持仓方向检查靠 PositionGetInteger(POSITION_TYPE) 与 POSITION_TYPE_SELL 比较,原文里有一处少写了一个等号(= 而非 ==),编译必报错,实写时得补成双等号。CheckTrade 函数里用 for(int i=1; i<=PositionsTotal; i++) 遍历持仓,marker 初始 false,匹配到才翻 true。 取指标值统一走 CopyBuffer:CopyBuffer(maHandle,0,0,3,maVal) 表示拿 MA 主缓冲从 0 号柱往后 3 根;双 CCI 同理用 0,0,3 或 0,0,5 拉长度。返回值小于 0 就 Alert 并退出,保证数组里是有效数值再进后续条件判断。 开 MT5 新建 EA 把这段变量声明和 CopyBuffer 调用原样贴进去,故意把 PositionGetInteger 那行写成单等号,编译一次就能看到 159 行报 assignment instead of equality 的错。

MQL5 / C++
class="type">int cciHandle1,cciHandle2;   class=class="str">"cmt">// 我们CCI指标的句柄
class="type">class="kw">double maVal[];                class=class="str">"cmt">// 用于保存每个柱的移动平均值的动态数组
class="type">class="kw">double cciVal1[],cciVal2[]; class=class="str">"cmt">// 用于保存每个柱CCI指标值的动态数组
class="type">class="kw">double p1_close,p2_close;   class=class="str">"cmt">// 用于分别保存柱1和柱2收盘价的变量
class=class="str">"cmt">//--- 使用 MQL5 class="type">MqlTick 结构读取最新报价
   if(!SymbolInfoTick(_Symbol,latest_price))
     {
       Alert("读取最新报价出错 - 错误:",GetLastError,"!!");    class=class="str">"cmt">// 103行
       class="kw">return;
     }
class=class="str">"cmt">//--- 使用 MQL5 class="type">MqlTick 结构读取最新报价
   if(!SymbolInfoTick(_Symbol,latest_price))
     {
       Alert("读取最新报价出错 - 错误:",GetLastError(),"!!");    class=class="str">"cmt">// 103行
       class="kw">return;
     }
        else if(PositionGetInteger(POSITION_TYPE) = POSITION_TYPE_SELL) class=class="str">"cmt">// 159行
         {
          Sell_opened = true; class=class="str">"cmt">// 是卖出
        else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) class=class="str">"cmt">// 159行
         {
          Sell_opened = true; class=class="str">"cmt">// 是卖出
class="type">bool CheckTrade(class="type">class="kw">string otyp)
{
   class="type">bool marker = class="kw">false;
   for (class="type">int i=class="num">1; i<=PositionsTotal;i++)   class=class="str">"cmt">// 292行
   {
class=class="str">"cmt">//--- 使用句柄把我们指标的新值复制到缓冲区(数组)中
   if(CopyBuffer(maHandle,class="num">0,class="num">0,class="num">3,maVal)<class="num">0)
     {
       Alert("复制 MA 指标缓冲区出错 - 错误:",GetLastError(),"!!");
       class="kw">return;
     }
   if(CopyBuffer(cciHandle1,class="num">0,class="num">0,class="num">3,cciVal1)<class="num">0 || CopyBuffer(cciHandle2,class="num">0,class="num">0,class="num">3,cciVal2)<class="num">0)
     {
       Alert("复制 CCI 指标缓冲区出错 - 错误:",GetLastError());
       class="kw">return;
     }
class=class="str">"cmt">//--- 使用句柄把我们指标的新值复制到缓冲区(数组)中
   if(CopyBuffer(maHandle,class="num">0,class="num">0,class="num">5,maVal)<class="num">0)
     {
       Alert("复制MA指标缓冲区出错 - 错误:",GetLastError(),"!!");
       class="kw">return;
     }
   if(CopyBuffer(cciHandle1,class="num">0,class="num">0,class="num">5,cciVal1)<class="num">0 || CopyBuffer(cciHandle2,class="num">0,class="num">0,class="num">5,cciVal2)<class="num">0)
     {
       Alert("复制CCI指标缓冲区出错 - 错误:",GetLastError());
       class="kw">return;
     }

用 MT5 策略测试器给 EA 跑全市场初筛与遗传优化

代码写完先别急着上实盘,第一步该在 MT5 内置的策略测试器里做无错验证后的参数普查。从 View 菜单进 Strategy Tester,Settings 里挂上你的 Expert,时间框架先定好,Optimization 字段选『Market Watch 中所有品种』,优化目标挑 Balance + max Profit Factor,价格变动用 Every Tick,Forward 留 No。这样一轮跑完,Journal 提示完成后去 Optimization Results 按 Result 排序,就能看出哪些品种在当下周期能盈利。 我们那版 EA 在 1 小时框下初筛,EURUSD、EURJPY、AUDUSD 三个品种给出了正收益;这一步只是定位战场,不负责细调。接着锁定 EURUSD + H1 做正式优化,Settings 里开 Optimization、用 Fast Genetic Based Algorithm,目标依旧是 Balance + Max Profit Factor。Inputs 选项卡只勾黄区参数,比如止损、止盈、MA 周期、双 CCI 周期,勾得越多组合数越爆炸。 以止损为例:Start 30、Step 2、Stop 38,代表测试器只在 30~38 之间取 2 的倍数(30/32/34/36/38 共 5 种),不会越界到 40。五个参数叠加后组合数可达数千次,代理机数量决定耗时;若只想快点出结果,只留两三个真正驱动策略的变量即可,通常就是指标周期。 优化完在 Optimization Results 右键补出 Profit Factor、Drawdown%、Expected Payoff 等列。我们那次结果里,A 区(黄)Balance+PF 22381.71、盈利 924.10、回撤 1.78%;B 区(绿)第二项 22159.25、盈利 936.55、回撤 1.95%。外汇与贵金属属高风险品种,回撤数字只代表历史测试样本,实盘可能显著放大。 切到 Optimization Graph 右键调 1D Graph,按 X 轴逐个参数看峰值:止损最佳 34、止盈 78、MA_Period 26、CCI_Period1 62、CCI_Period2 取 28 或 30。把这些值填回 Inputs,Settings 关掉 Optimization 跑最终测试,Results 和 Graph 选项卡会给出单组参数下的权益曲线。整套 XML 缓存在 Tester\cache\cci_ma_ea.EURUSD.H1.0.xml,可用 Excel 直接剥开看每代数据。

「画得少,看得清」

把编辑器里的编译报错先归零,再进策略测试器跑历史报价,这是写出能用的 EA 的底线动作。前面附的 cci_ma_ea.mq5 与 cci_ma_ea_corrected.mq5 相差仅 0.02 KB,区别就在几处数组越界和订单检查逻辑,改完之后回测才不会中途崩掉。 外汇和贵金属杠杆高、滑点跳空频繁,同一套参数在 2023 年 XAUUSD 上回测夏普可能为正,换到 2024 年就可能连续回撤,所以每调一次参数都该重跑一遍可视化测试。 真正省时间的做法,是让 MT5 把每笔撮合画在 K 线上,你只盯着那几根关键的 CCI 交叉,而不是翻几千行日志。看得清进出场,比堆功能重要得多。

常见问题

测试多基于历史拟合,滑点、点差与隔夜费未完全计入;实盘前先用模拟盘跑两周验证样本外表现。
检查变量或函数是否漏写 #include 或拼写错误;逐条清掉报错可从第一行往上修,避免连带误报。
小布盯盘的 AIGC 会叠加 EA 逻辑与实时价格行为,打开对应品种页即可看到信号一致度诊断。
MT5 净仓模式下用 PositionsTotal 查当前持仓;OrdersTotal 含挂单,误用会重复开仓。
先 50 代粗筛淘汰明显负期望品种,再对留存品种做 200 代细调,兼顾效率与过拟合风险。