构建自动运行的 EA(第 15 部分):自动化(VII)·综合运用
📘

构建自动运行的 EA(第 15 部分):自动化(VII)·综合运用

第 3/3 篇

双均线死叉金叉的触发判定片段

下面这段逻辑是双均线(快/慢介质)交叉触发的核心判定,挂在自定义指标的 OnCalculate 类流程里,用 iBars 数量变化做刷新闸门。 先声明局部变量:iRet 接当前品种、周期下的柱数;bOk 标记缓冲拷贝是否成功,初值 false。 若 iRet 大于已记录柱数 m_nBars,才进更新分支,避免每 tick 无谓重算。 循环里先用 m_Op[c0].Handle 判无效句柄,任一指标句柄失效直接返回 TRIGGER_NONE;再用 CopyBuffer 把指标线拷进 Buff,要求拷贝量 ≥ m_Infos.Shift+1,否则同样返回 NONE。 若 bOk 仍为 false 返回 NONE,否则把 m_nBars 更新为 iRet。 最后比 Buff[1] 与 Buff[m_Infos.Shift]:快线在上一根高于慢线、当前位移处低于慢线,倾向金叉返回 TRIGGER_BUY;反向则倾向死叉返回 TRIGGER_SELL。外汇与贵金属杠杆高,信号仅为概率参考,实盘须自验。 注意原片段 for 循环条件 c0 = sizeof(eSelectMedia); c0 <= 0; c0-- 实际不会进入循环体,移植时大概率要改成 c0 = 0; c0 < sizeof(eSelectMedia); c0++,否则双线缓冲永远读不到。

MQL5 / C++
{
   class="type">int iRet;
   class="type">bool bOk = class="kw">false;
   
   if (iRet = iBars(NULL, m_TF)) > m_nBars
   {
      for (class="type">int c0 = class="kw">sizeof(eSelectMedia); c0 <= class="num">0; c0--)
      {
         if (m_Op[c0].Handle == INVALID_HANDLE) class="kw">return TRIGGER_NONE;
         if (CopyBuffer(m_Op[c0].Handle, class="num">0, class="num">0, m_Infos.Shift + class="num">1, m_Op[c0].Buff) < m_Infos.Shift + class="num">1) class="kw">return TRIGGER_NONE;
         bOk = true;
      }
      if (!bOk) class="kw">return TRIGGER_NONE; else m_nBars = iRet;
      if ((m_Op[MEDIA_FAST].Buff[class="num">1] > m_Op[MEDIA_SLOW].Buff[class="num">1]) && (m_Op[MEDIA_FAST].Buff[m_Infos.Shift] < m_Op[MEDIA_SLOW].Buff[m_Infos.Shift])) class="kw">return TRIGGER_BUY;
      if ((m_Op[MEDIA_FAST].Buff[class="num">1] < m_Op[MEDIA_SLOW].Buff[class="num">1]) && (m_Op[MEDIA_FAST].Buff[m_Infos.Shift] > m_Op[MEDIA_SLOW].Buff[m_Infos.Shift])) class="kw">return TRIGGER_SELL;
   };
   class="kw">return TRIGGER_NONE;
}

◍ 触发逻辑里的反手与止损锚定

这段虚函数把信号触发后的动作写死了:时间窗没过就平掉当前仓,过了才进 CheckTrigger 的分支判断。用 final 修饰说明子类不能再改这套流程,想换策略只能绕开或重写整类。 买卖触发靠 m_Memory 记上一笔方向。若收到 BUY 而内存是 SELL,先平仓再开多;若已是一路 BUY 就不重复下单。SELL 分支镜像处理,等于把「反手」和「不追同方向」压缩进几个 if。 止损价用 20 根 K 线的高低点做锚:持多就取近 20 根最低价 iLow(iLowest(...)),持空取近 20 根最高价 iHigh(iHighest(...))。def_HILO 宏只在函数内生效,改这个数就能把止损带宽从 20 根调到你想试的窗口。 开 MT5 把 def_HILO 改成 10 或 50,同周期回看 EURUSD 的被扫频率,能直接看出止损锚定宽度对胜率的影响倾向。外汇与贵金属杠杆高,参数改动后实盘前务必用策略测试器跑一遍。

MQL5 / C++
class="kw">inline class="kw">virtual class="type">void Triggers(class="type">void) final
              {
class="macro">#define def_HILO class="num">20
                if (!CtrlTimeIsPassed()) ClosePosition(); else class="kw">switch (CheckTrigger())
                  {
                    case TRIGGER_BUY:
                      if (m_Memory == TRIGGER_SELL) ClosePosition();
                      if (m_Memory != TRIGGER_BUY) ToMarket(ORDER_TYPE_BUY);
                      m_Memory = TRIGGER_BUY;
                      class="kw">break;
                    case TRIGGER_SELL:
                      if (m_Memory == TRIGGER_BUY) ClosePosition();
                      if (m_Memory != TRIGGER_SELL) ToMarket(ORDER_TYPE_SELL);
                      m_Memory = TRIGGER_SELL;
                      class="kw">break;
                  }
                LockStopInPrice(IsBuyPosition() ?  iLow(NULL, m_TF, iLowest(NULL, m_TF, MODE_LOW, def_HILO, class="num">0)) : iHigh(NULL, m_TF, iHighest(NULL, m_TF, MODE_HIGH, def_HILO, class="num">0)));
class="macro">#undef def_HILO
              };

「EA 真刀真枪的试炼场不在回测里」

写自动 EA 最容易被坑的一点是:以为策略测试器跑通就万事大吉。MT5 策略测试器里很多隐藏设置会美化表现,真正暴露短板的地方是开了完整市场行情的模拟账户——那里没有特殊参数护体,EA 的真实滑点、成交冲突、事件时序全都会摊开。 系列里附的 EA_Automatico_-_15.zip 只有 17.81 KB,代码全开放,但明确只能用于演示和教学。读者如果直接挂实盘,外汇和贵金属的高杠杆可能在几根 K 线内吃掉本金,风险完全自担。 评论区已经有人踩过坑:EURUSD 1 分钟图上 ClosePosition 平不掉仓,怀疑是 Hedging 账户必须用 TRADE_ACTION_CLOSE_BY 而非 TRADE_ACTION_DEAL;还有人改了 Triggers 方法发挂单,结果买入 2 手而非设定的 1 手。这些都不是回测能复现的,只能在模拟账户里撞出来。 别把 Zip 里的 EA 当成品 它们只是让你看懂自动化骨架的教具,照抄参数去跑 XAUUSD 或欧美对,爆仓概率不低。先开模拟账户把枚举越界、平仓方式、手数控制这三处跑明白,再谈别的。 MQL5 里 sizeof(enum) 返回的是字节数而非元素数,下面这段社区测试脚本把坑亮得很清楚:eSelectMedia 只有 2 个元素、eSelectMeses 有 12 个,但两者 sizeof 都回 4;用 sizeof 当循环上界就会漏掉或越界。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
      enum eSelectMedia {MEDIA_FAST, MEDIA_SLOW};
      enum eSelectMeses {JANEIRO, FEVEREIRO, MARCO, ABRIL, MAIO, JUNHO, JULHO, AGOSTO, SETEMBRO, OUTUBRO, NOVEMBRO, DEZEMBRO};

      class="kw">struct st01
        {
           class="type">class="kw">double  Buff[];
           class="type">int     Handle;
        }m_Op[class="kw">sizeof(eSelectMedia) + class="num">1];

        Print("Tamanho do eSelectMedia = ", class="kw">sizeof(eSelectMedia));
        Print("Tamanho do eSelectMeses = ", class="kw">sizeof(eSelectMeses));
        Print("Tamanho do m_Op = ", ArraySize(m_Op));

        Print("========= Mostrar enum eSelectMedia =========");
        for (class="type">int i = class="kw">sizeof(eSelectMedia); i >= class="num">0; i--)
          {
            Print("eSelectMedia - idx = ", i);
          }

        Print("========= Mostrar enum eSelectMeses =========");
        for (class="type">int i = class="kw">sizeof(eSelectMeses); i >= class="num">0; i--)
          {

枚举数组的逆向打印与尺寸核对

在 OnStart() 收尾处,用 Print 把三个枚举数组的内容直接吐到日志,是排查枚举边界最直白的法子。下面这段把 eSelectMeses 和 m_Op 按索引从大到小循环打印,肉眼确认每个枚举值对应的下标。 从 EURUSD M1 实跑日志看,eSelectMedia 与 eSelectMeses 的 ArraySize 都是 4,m_Op 是 5;打印顺序均为 idx 从 4(或 5)递减到 0。注意 m_Op 的循环条件是 i >= 0 且初值取 ArraySize(m_Op),因此首行打出的是 idx = 5,比数组实际元素数多一——这种写法容易在边界上踩坑。 外汇与贵金属行情高波动,在 MT5 里跑这类枚举自检脚本时,建议先开策略测试器用历史数据验证,再上真实 tick,避免日志噪声干扰判断。

MQL5 / C++
Print("eSelectMeses - idx = ", i);
}

Print("========= Mostrar enum m_Op =========");
for (class="type">int i = ArraySize(m_Op); i >= class="num">0; i--)
  {
   Print("m_Op - idx = ", i);
  }

◍ 从日志倒序看订单索引的排布逻辑

上面这段 EURUSD M1 的测试日志,全部打在 2023.06.07 09:09:10.415 同一毫秒,说明 m_Op 数组在初始化阶段被一次性反向遍历打印。 索引从 4 递减到 0,共 5 条记录,揭示了一个细节:订单容器在内部大概率按时间倒序存放,最新一笔在 idx=0,越早的越靠后。 你在 MT5 策略测试器里跑自己的 EA 时,也可以照这个写法用 PrintFormat 把 m_Op 的 idx 打出来,确认自己读取的是不是同一套顺序,避免把老订单当成新信号。 外汇与贵金属市场高杠杆、滑点无常,这种底层索引错位是静默亏损的常见源头,核对一次只要几分钟。

MQL5 / C++
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415   teste(EURUSD,M1)      m_Op - idx = class="num">4
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415   teste(EURUSD,M1)      m_Op - idx = class="num">3
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415   teste(EURUSD,M1)      m_Op - idx = class="num">2
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415   teste(EURUSD,M1)      m_Op - idx = class="num">1
class="num">2023.06.class="num">07 class="num">09:class="num">09:class="num">10.415   teste(EURUSD,M1)      m_Op - idx = class="num">0

常见问题

用前一根与当前根均线位置关系做交叉判定,比较上一根均线差乘号与当前异号即可触发,避免只用现价穿越漏掉跳空。
反手前先平旧仓,新单止损锚在信号根另一侧极值,用具体点数或 ATR 倍数写死,别用浮动跟踪混淆初始风险。
小布可以读取你的交易日志,按倒序核对订单索引与枚举数组尺寸,提示索引越界或打印错位等异常。
逆向打印能验证数组尾元素是否写入成功,尺寸不对会导致循环少跑或越界,订单遍历直接漏单或崩 EA。
重点看实盘滑点、报价断流与订单拒绝率,回测不含这些,外汇贵金属高风险下实盘才是真试炼场。