跨平台的EA交易: 信号·进阶篇
(2/3)· 从订单管理到信号引擎,理清进场出场四类指令在MQL4/MQL5下的兼容实现
◍ 信号聚合逻辑的差异点
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 会先置中性,再迭代各信号,一旦某个信号无效或与之前有效信号冲突,最终输出即判为无效。外汇与贵金属波动剧烈、杠杆风险高,信号冲突逻辑务必在回测中验证。
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。
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,基本只能近似重构甚至放弃。外汇与贵金属杠杆高,跨平台迁移前务必在模拟盘核对信号一致性。