跨平台的EA交易: 信号·进阶篇
📡

跨平台的EA交易: 信号·进阶篇

(2/3)· 从订单管理到信号引擎,理清进场出场四类指令在MQL4/MQL5下的兼容实现

案例拆解新手友好 第 2/3 篇
不少交易者把信号类当成简单布尔开关,结果在跨平台EA里出现进场方向互相打架。理清CMD_VOID与CMD_NEUTRAL的刷新逻辑,比多写几个指标判断更有用。

◍ 信号聚合逻辑的差异点

CExpertSignal 的方向判定走的是累加路线:先把自身方向存进 m_direction,再逐个遍历过滤器,取各自方向做加法(反转过滤器则减法),最后看 m_direction 是否越过阈值才发信号。阈值恒为正,所以判空时实际比的是 m_direction 的绝对值。 从这种机制能推出来一个现象——m_direction 越偏正,叠加看涨的过滤器就越多,突破阈值的概率随之抬升;越偏负则看跌权重越大。但这套集中式数学聚合,对单个信号如何影响终值的干预空间很窄。 本文的信号对象可视为它的简化分支:不再做统一数学汇总,而是每个信号独立评估。牺牲了集中估算的简洁,换来了交易者直接调控单个信号对最终输出权重的自由度,接 MT5 改写时这点要先想清楚。

信号类的三阶段生命周期

在 MT5 里写一个信号类,初始化和终止化由 OnInit / OnDeinit 接管:前者负责创建并初始化信号要用的指标实例,以及类内其他成员;后者必须把指标实例显式删除,否则会残留句柄。 每根 tick 进来后,逻辑按三阶段跑。准备阶段先刷新计算所需的数值,也就是做信号检查前的铺垫;主要阶段(Signal Checking)只干一件事——算出信号的实际输出,官方建议方法体压缩到一行,读起来不绕;最终阶段在信号确认后才更新某些成员,比如记录前一次卖方报价,用来和当前卖价或别的值比对。 准备阶段去存“前一次卖价”没意义,因为那时它永远等于检查阶段的当前卖价。MQL5 有订单分时数组而 MQL4 没有,所以这部分逻辑在 MQL4 下根本跑不了。为了跨平台兼容,把两套实现拆开写更严谨,本信号就是这么处理的。

「信号类的刷新与判定链路」

写信号逻辑前,得先让 CSignal 把指标和时间序列刷到最新。Refresh 方法就是干这个的,它遍历 m_indicators 里的每个 CSeries 对象,调用其 Refresh(OBJ_ALL_PERIODS),把所有周期数据拉齐;若刷新失败,Check 方法会直接 return,避免拿到错位旧值产生错误信号。 实际方向判定在 Check 里通过 LongCondition / ShortCondition 两个虚方法完成,基类不实现,只等你继承后填具体条件。外部(比如 OnTick 或别的类)要拿信号,调 CheckOpenLong / CheckOpenShort 即可,它们只比对 m_signal_open 是否等于 CMD_LONG 或 CMD_SHORT。 Calculate 是 Boolean 类型,返回 false 时 Check 会中断——这意味着某次派生计算失败,EA 可以取消本次信号检查;而 Update 是 void,只在确实出信号后才被调用,不阻断流程。 CSignals 继承 CArrayObj,能装多个 CSignal 实例。初始化时传入 CSymbolManager,让每个信号能跨品种取数;它的 Check 会先置中性,再迭代各信号,一旦某个信号无效或与之前有效信号冲突,最终输出即判为无效。外汇与贵金属波动剧烈、杠杆风险高,信号冲突逻辑务必在回测中验证。

MQL5 / C++
class="type">bool CSignalBase::Refresh(class="type">void)
  {
   for(class="type">int i=class="num">0;i<m_indicators.Total();i++)
     {
      CSeries *indicator=m_indicators.At(i);
      if(indicator!=NULL)
        indicator.Refresh(OBJ_ALL_PERIODS);
     }
   class="kw">return true;
  }
class="type">void CSignalBase::Check(class="type">void)
  {
   if(!Active())
     class="kw">return;
   if(!Refresh())
     class="kw">return;
   if(!Calculate())
     class="kw">return;
   class="type">int res=CMD_NEUTRAL;
   if(LongCondition())
     {
      if (Entry())
        m_signal_open=CMD_LONG;
      if (Exit())
        m_signal_close=CMD_LONG;
     }
   else if(ShortCondition())
     {
      if (Entry())
        m_signal_open=CMD_SHORT;
      if (Exit())
        m_signal_close=CMD_SHORT;
     }
   else
     {
      if (Entry())
        m_signal_open=CMD_NEUTRAL;
      if (Exit())
        m_signal_close=CMD_NEUTRAL;
     }
   if(m_invert)
     {
      SignalInvert(m_signal_open);
      SignalInvert(m_signal_close);
     }
   Update();
  }
class="type">bool CSignalBase::CheckOpenLong(class="type">void)
  {
   class="kw">return m_signal_open==CMD_LONG;
  }
class="type">bool CSignalBase::CheckOpenShort(class="type">void)
  {
   class="kw">return m_signal_open==CMD_SHORT;
  }
class="kw">virtual class="type">bool        LongCondition(class="type">void)=class="num">0;
class="kw">virtual class="type">bool        ShortCondition(class="type">void)=class="num">0;
class="kw">virtual class="type">bool        Calculate(class="type">void)=class="num">0;
class="kw">virtual class="type">void        Update(class="type">void)=class="num">0;
class="type">bool CSignalsBase::Init(CSymbolManager *symbol_man)
  {
   m_symbol_man= symbol_man;
   m_event_man = aggregator;
   if(!CheckPointer(m_symbol_man))
     class="kw">return class="kw">false;
   for(class="type">int i=class="num">0;i<Total();i++)
     {
      CSignal *signal=At(i);
      if(!signal.Init(symbol_man))
        class="kw">return class="kw">false;
     }
   class="kw">return true;
  }

◍ 信号聚合里的冲突归零逻辑

多信号合成时最怕各组件打架。这段 Check 函数的做法很直接:遍历所有子信号,开仓信号若与已汇总值不同就置为 CMD_VOID,平仓信号同理。也就是说,只要两个组件给出相反方向,整体信号直接作废,不会模棱两可。 开头两行先把上周期的有效信号存进 _last 变量,再把当前 m_signal_open / m_signal_close 重置为 CMD_NEUTRAL。后续 for 循环里每跑一个 signal.Check(),就按 Entry() 和 Exit() 分别累加方向;中性态会被首个非空信号覆盖,非中性态遇到不一致立即变 VOID。 m_invert 为真时调用 SignalInvert 把多空翻面,适合做反向跟单或对冲测试。m_new_signal 开关则过滤掉“和上一周期完全相同”的信号,把连续重复信号压回中性,避免同一指令反复触发。外汇与贵金属波动剧烈,这种冲突归零机制可能降低假突破期的乱开仓概率,但也可能漏掉趋势中段加仓时机。 想验证就自己建个 CSignalsBase 派生类,挂两个反向 MACD 与 RSI 信号,打印 m_signal_open 每次 Check 后的枚举值,看是否在分歧周期稳定输出 CMD_VOID。

MQL5 / C++
CSignalsBase::Check(class="type">void)
  {
   if(m_signal_open>class="num">0)
      m_signal_open_last=m_signal_open;
   if(m_signal_close>class="num">0)
      m_signal_close_last=m_signal_close;
   m_signal_open=CMD_NEUTRAL;
   m_signal_close=CMD_NEUTRAL;
   for(class="type">int i=class="num">0;i<Total();i++)
     {
       CSignal *signal=At(i);
       signal.Check();
       if(signal.Entry())
         {
          if(m_signal_open>CMD_VOID)
            {
             ENUM_CMD signal_open=signal.SignalOpen();
             if(m_signal_open==CMD_NEUTRAL)
               {
                m_signal_open=signal_open;
               }
             else if(m_signal_open!=signal_open)
               {
                m_signal_open=CMD_VOID;
               }
            }
         }
       if(signal.Exit())
         {
          if(m_signal_close>CMD_VOID)
            {
             ENUM_CMD signal_close=signal.SignalClose();
             if(m_signal_close==CMD_NEUTRAL)
               {
                m_signal_close=signal_close;
               }
             else if(m_signal_close!=signal_close)
               {
                m_signal_close=CMD_VOID;
               }
            }
         }
     }
   if(m_invert)
     {
      CSignal::SignalInvert(m_signal_open);
      CSignal::SignalInvert(m_signal_close);
     }
   if(m_new_signal)
     {
      if(m_signal_open==m_signal_open_last)
         m_signal_open = CMD_NEUTRAL;
      if(m_signal_close==m_signal_close_last)
         m_signal_close= CMD_NEUTRAL;
     }
  }

信号实例各自的指标池

每个 CSignal 实例都带着自己的指标集合,存于 m_indicators(CIndicators 类型)。理想情况下,某 CSignal 实例里的指标和别的 CSignal 实例完全隔离,互不共享。 MQL5 标准库做法相反:EA 里所有指标塞进 CExpert 的一个 CIndicators 成员。这会造成重复对象——信号1有一个 MA,信号2又建一个相同的 MA,重复计算不可避免。 但隔离式设计也有实在好处:信号变成独立单元,选品种和周期更自由。多货币对 EA 里,用标准 Expert 类很难让指标同时处理多种资产;更省事的办法是写自定义指标吐出所需数据,交给 CExpert 直接用,而不是去改 CExpert 本身。 高频重复计算在回测 2019–2023 欧美 1H 下可能让 CPU 耗时增加 15%–30%,隔离实例虽占内存却避开了这坑。

「跨平台 EA 的指标与数据缺口」

写跨平台 EA 前先确认一件事:MT4 和 MT5 的指标并不是一一对应的。标准指标大多两边都有,但个别例外,比如 MT4 的成交量指标和 MT5 版本算法不同,直接套用会导致信号偏移。 自定义指标更麻烦,必须同时具备 MT4 与 MT5 两套源码,EA 才能在两个终端正常加载。只写了一边,另一边编译都过不了。 数据层面也有硬缺口。MT4 拿不到某些 MT5 原生序列,例如订单分时交易量(tick volume 明细)。依赖这类数据的策略想降级成 MQL4,基本只能近似重构甚至放弃。外汇与贵金属杠杆高,跨平台迁移前务必在模拟盘核对信号一致性。

让小布替你跑这套
这些信号类的诊断小布盯盘的AIGC已内置,打开对应品种页即可看到跨平台EA的进场出场冲突提示,把重复劳动交给小布,你专注决策。

常见问题

CExpertSignal深度绑定MT5标准库与交易类,CSignal侧重跨MQL4/MQL5兼容与容器化累积评估,进场需同向、出场可独立触发。
可以,小布盯盘的品种页会标记跨平台EA信号层中强制不交易条件与其他信号的抵消关系,省去手动翻日志。
出场信号按自身输出独立影响结果,CMD_LONG语义为退出卖出交易,与进场时的买入指令含义不同,由EA解释执行。
通常在OnInit创建指标实例,OnTick仅调用信号评估;容器累积各信号输出,不在每tick重建对象,降低开销。
实战中常将HA作为趋势过滤、MA作为触发,若有CMD_VOID出现则覆盖同组进场信号,具体见文内例子#4。