跨平台的EA交易: 信号·综合运用
📘

跨平台的EA交易: 信号·综合运用

第 3/3 篇

把信号逻辑塞进类里:四个跨平台EA样例

订单管理器那个例子里,原始 OnTick 把行为代码和信号生成混在一起。新柱开始时同时发买卖单,源码短还好读;EA 变复杂后维护成本会陡增。解决思路是用之前讨论过的 CSignal 类来托管信号生成。 扩展 CSignal 需要三个 protected 成员:前一根柱数量、再前一根柱数量、当前方向。Calculate 方法沿用旧算法,但返回值必须是 Boolean——返回 false 就中止后续信号处理。明确两条继续处理原则:当前柱数大于 0,且大于之前柱数。 Update 方法里把 m_bars_prev 刷成当前柱数,方向变量逻辑是:当前值 ≤0(空仓或首单)则新值设 1,否则设 -1。LongCondition / ShortCondition 则根据分时信号判定多空输出。 MA 例子直接用现成的移动平均类,MQL4/MQL5 库都给了封装。构造函数里必须初始化指标,Init 里用 CSymbolInfo 指针绑交易品种。信号条件极简:收盘价高于信号柱 MA 即多,低于即空。MT4 与 MT5 回测执行同一逻辑。 HA 例子麻烦在它是自定义指标。CiHA 类要重写 Create 和 GetData:MQL5 用 iCustom 拿句柄再取数,MQL4 直接 iCustom 取柱值。缓冲区排布不同——MQL5 是开高低收(0~3),MQL4 是低高开收(0~3 中开在 2)。HA 收盘两平台都在缓冲区 3。OnInit 里指标路径也要分开:MQL5 默认 Indicators\Examples\Heiken Ashi,MQL4 在 Indicators\Heiken Ashi。 最后一个例子把 MA 和 HA 信号类实例都加进 CSignals 容器,无新架构。四个样例在 MT4/MT5 回测均跑通相同逻辑,但外汇与贵金属杠杆高、滑点跳空频繁,跨平台复刻不代表实盘概率一致,开 MT5 用策略测试器跑同一 ex5 才能验证。

MQL5 / C++
<span class="keyword">class="type">void</span> <span class="functions">OnTick</span>()
  {
<span class="comment">class=class="str">"cmt">//---</span>
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">int</span> bars = <span class="number">class="num">0</span>;
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">int</span> direction = <span class="number">class="num">0</span>;
   <span class="keyword">class="type">int</span> current_bars = <span class="number">class="num">0</span>;
   <span class="preprocessor">class="macro">#ifdef </span><span class="macro">__MQL5__</span>
      current_bars = <span class="predefines">Bars</span>(<span class="macro">NULL</span>,<span class="macro">PERIOD_CURRENT</span>);
   <span class="preprocessor">class="macro">#else
      </span>current_bars = <span class="predefines">Bars</span>;
   <span class="preprocessor">class="macro">#endif
   </span><span class="keyword">if</span> (bars&lt;current_bars)
   {  
      symbol_info.<span class="functions">RefreshRates</span>();
      COrder *last = order_manager.LatestOrder();
      <span class="keyword">if</span> (<span class="functions">CheckPointer</span>(last) &amp;&amp; !last.IsClosed())
         order_manager.CloseOrder(last);
      <span class="keyword">if</span> (direction&lt;=<span class="number">class="num">0</span>)
      {

◍ 用信号类切换多空方向的实盘写法

这段代码演示了如何在 MT5 里用自定义信号类接管开仓逻辑,而不是在 OnTick 里硬写条件。SignalOrderManagerExample 继承 CSignal,核心靠 m_direction 在每次 Update 时翻转符号,从而交替触发多空。 Calculate 里用 Bars(NULL,PERIOD_CURRENT) 取当前周期 K 线数,返回条件要求 m_bars>0 且大于上次的 m_bars_prev,也就是必须有新柱才重算。注意 MQL5 下 Bars 是函数不是属性,老写法在 MQL5 编译会直接报错。 LongCondition 返回 m_direction<=0,ShortCondition 返回 m_direction>0,两者互斥。OnInit 中把 signal_ordermanager 指针加入 CSignals 容器,OnTick 里先 symbol_info.RefreshRates() 再 signals.Check(),由框架决定 CheckOpenLong / CheckOpenShort。 外汇和贵金属杠杆高,这种每根新柱翻转方向的策略可能频繁反向,实盘前务必在策略测试器用至少 3 个月 Tick 数据回测验证滑点下的表现。

MQL5 / C++
Print("进入买入交易..");
order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
direction = class="num">1;
}
else
{
 Print("进入卖出交易..");
 order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
 direction = -class="num">1;
} 
 bars = current_bars;
 }
}
class SignalOrderManagerExample: class="kw">public CSignal
 {
class="kw">protected:
 class="type">int m_bars_prev;
 class="type">int m_bars;
 class="type">int m_direction;
 class=class="str">"cmt">//类的剩余部分
class="type">bool SignalOrderManagerExample::Calculate(class="type">void)
 {
 class="macro">#ifdef __MQL5__
 m_bars=Bars(NULL,PERIOD_CURRENT);
 class="macro">#else
 m_bars=Bars;
 class="macro">#endif
 class="kw">return m_bars>class="num">0 && m_bars>m_bars_prev;
 }
class="type">void SignalOrderManagerExample::Update(class="type">void)
 {
 m_bars_prev=m_bars;
 m_direction= m_direction<=class="num">0?class="num">1:-class="num">1;
 }
class="type">bool SignalOrderManagerExample::LongCondition(class="type">void)
 {
 class="kw">return m_direction<=class="num">0;
 }
class="type">bool SignalOrderManagerExample::ShortCondition(class="type">void)
 {
 class="kw">return m_direction>class="num">0;
 }
class="type">int OnInit()
 {
class=class="str">"cmt">//---
 order_manager=new COrderManager();
 symbol_manager=new CSymbolManager();
 symbol_info=new CSymbolInfo();
 if(!symbol_info.Name(Symbol()))
 Print("没有设置交易品种");
 symbol_manager.Add(GetPointer(symbol_info));
 order_manager.Init(symbol_manager,NULL);
 SignalOrderManagerExample *signal_ordermanager=new SignalOrderManagerExample();
 signals=new CSignals();
 signals.Add(GetPointer(signal_ordermanager));
class=class="str">"cmt">//---
 class="kw">return(INIT_SUCCEEDED);
 }
class="type">void OnTick()
 {
class=class="str">"cmt">//---
 symbol_info.RefreshRates(); 
 signals.Check();
 if(signals.CheckOpenLong())
 {
 close_last();
 Print("进入买入交易..");
 order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
 }
 else if(signals.CheckOpenShort())
 {
 close_last();
 Print("进入卖出交易..");
 order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
 }
 }
class SignalMA: class="kw">public CSignal
 {
class="kw">protected:
 CiMA *m_ma;
 CSymbolInfo *m_symbol;

「把均线信号塞进类里跑OnTick」

下面这段把均线穿越逻辑封装成 SignalMA 类,核心成员只留了品种名、周期、信号柱和收盘价,其余交给基类。构造函数里直接 new 一个 CiMA 并 Create,period 和 method 由外部传参,实盘里改这两个值就能换均线类型。 Calculate 只做一件事:用 CopyClose 取信号柱那一根的收盘价,成功才返回 true。LongCondition 和 ShortCondition 极简——收盘价大于均线主值看多,小于看空,没有过滤。外汇和贵金属波动大,这种裸穿越在震荡市可能频繁挨巴掌。 实例化时把 Symbol() 和 Period() 直接喂进去,signals.Check() 在 OnTick 每跳调用;CheckOpenLong 为真就平旧仓接 Ask 价买,CheckOpenShort 则接 Bid 价卖。注意 OnTick 里没设滑点保护和手数变量,复制去 MT5 跑之前至少补一个最小止损距离判断。 类尾的 CiHA 只是个开头,继承 CiCustom,说明后续还能挂自定义指标信号,但本节没给实现体,别指望直接编译通过。

MQL5 / C++
class="type">class="kw">string m_symbol_name;
ENUM_TIMEFRAMES m_timeframe;
class="type">int m_signal_bar;
class="type">class="kw">double m_close; 
class=class="str">"cmt">//类的剩余部分
class="type">void SignalMA::SignalMA(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int period,class="kw">const class="type">int shift,class="kw">const ENUM_MA_METHOD method,class="kw">const ENUM_APPLIED_PRICE applied,class="kw">const class="type">int bar)
  {
  m_symbol_name= symbol;
  m_timeframe = timeframe;
  m_signal_bar = bar;
  m_ma=new CiMA();
  m_ma.Create(symbol,timeframe,period,class="num">0,method,applied);
  m_indicators.Add(m_ma);  
  }
class="type">bool SignalMA::Init(CSymbolManager *symbol_man,CEventAggregator *event_man=NULL)
  {
  if(CSignal::Init(symbol_man,event_man))
     {
       if(CheckPointer(m_symbol_man))
          {
           m_symbol=m_symbol_man.Get();
           if(CheckPointer(m_symbol))
              class="kw">return true;
          }
     }
  class="kw">return class="kw">false;
  }
class="type">bool SignalMA::Calculate(class="type">void)
  {
  class="type">class="kw">double close[];
  if(CopyClose(m_symbol_name,m_timeframe,signal_bar,class="num">1,close)>class="num">0)
     {
      m_close=close[class="num">0];
      class="kw">return true;
     }  
  class="kw">return class="kw">false;
  }
class="type">bool SignalMA::LongCondition(class="type">void)
  {
  class="kw">return m_close>m_ma.Main(m_signal_bar);
  }
class="type">bool SignalMA::ShortCondition(class="type">void)
  {
  class="kw">return m_close<m_ma.Main(m_signal_bar);
  }
SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,class="num">0,mamethod,maapplied,signal_bar);
signals=new CSignals();
signals.Add(GetPointer(signal_ma));
signals.Init(GetPointer(symbol_manager),NULL);
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   symbol_info.RefreshRates();
   signals.Check();
   if(signals.CheckOpenLong())
      {
       close_last();
       Print("进入买入交易..");
       order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
      }
   else if(signals.CheckOpenShort())
      {
       close_last();
       Print("进入卖出交易..");
       order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
      }
  }
class CiHA: class="kw">public CiCustom
  {
class="kw">public:
                     CiHA(class="type">void);
                    ~CiHA(class="type">void);

在自定义类里封装 HA 信号判定

把 Heiken Ashi 做成可复用信号类,核心是先建一个 CiHA 子类接管指标句柄与缓冲读取。Create 里写死 4 个缓冲区(m_ha.Create(...,4)),对应 HA 的 open/high/low/close 四线,MT5 下走 CiCustom::GetData,MT4 则退回 iCustom 调用。 SignalHA 构造函数把 symbol、周期、参数数组和信号柱下标传进去,new 出 CiHA 并挂到 m_indicators 容器;Calculate 里 MQL5 取 buffer 0 作开盘、buffer 3 作收盘,MQL4 下开盘要取 buffer 2,这是两套环境缓冲排序不一致导致的坑,直接抄容易读错线。 多空条件就两行:收盘大于开盘倾向做多(m_open<m_close),开盘大于收盘倾向做空。外汇与贵金属杠杆高,HA 平滑后滞后明显,信号仅作概率参考,实盘前务必在 MT5 用 Examples\Heiken_Ashi 跑一遍缓冲序号。 OnInit 中 MqlParam 只塞一个字符串参数指向 HA 指标名,MQL5 用 "Examples\\Heiken_Ashi",MQL4 用 "Heiken Ashi",路径差一级反斜杠就会初始化失败,开 MT5 验证时先 Print 一下 Name 是否生效。

MQL5 / C++
  class="type">bool                Create(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period,
                         class="kw">const ENUM_INDICATOR type,class="kw">const class="type">int num_params,class="kw">const class="type">MqlParam &params[],class="kw">const class="type">int buffers);
  class="type">class="kw">double              GetData(class="kw">const class="type">int buffer_num,class="kw">const class="type">int index) class="kw">const;
  };
class="type">bool CiHA::Create(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period,class="kw">const ENUM_INDICATOR type,class="kw">const class="type">int num_params,class="kw">const class="type">MqlParam &params[],class="kw">const class="type">int buffers)
  {
  NumBuffers(buffers);
  if(CIndicator::Create(symbol,period,type,num_params,params))
     class="kw">return Initialize(symbol,period,num_params,params);
  class="kw">return class="kw">false;
  }
class="type">class="kw">double CiHA::GetData(class="kw">const class="type">int buffer_num,class="kw">const class="type">int index) class="kw">const
  {
  class="macro">#ifdef __MQL5__
     class="kw">return CiCustom::GetData(buffer_num,index);
  class="macro">#else
     class="kw">return iCustom(m_symbol,m_period,m_params[class="num">0].string_value,buffer_num,index);
  class="macro">#endif
  }
class="type">void SignalHA::SignalHA(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int numparams,class="kw">const class="type">MqlParam &params[],class="kw">const class="type">int bar)
  {
  m_symbol_name= symbol;
  m_signal_bar = bar;
  m_ha=new CiHA();
  m_ha.Create(symbol,timeframe,IND_CUSTOM,numparams,params,class="num">4);
  m_indicators.Add(m_ha);
  }
class="type">bool SignalHA::Calculate(class="type">void)
  {
  class="macro">#ifdef __MQL5__
     m_open=m_ha.GetData(class="num">0,signal_bar);
  class="macro">#else
     m_open=m_ha.GetData(class="num">2,signal_bar);
  class="macro">#endif
     m_close=m_ha.GetData(class="num">3,signal_bar);
  class="kw">return true;
  }
class="type">bool SignalHA::LongCondition(class="type">void)
  {
  class="kw">return m_open<m_close;
  }
class="type">bool SignalHA::ShortCondition(class="type">void)
  {
  class="kw">return m_open>m_close;
  }
class="type">int OnInit()
  {
class=class="str">"cmt">//---
  order_manager=new COrderManager();
  symbol_manager=new CSymbolManager();
  symbol_info=new CSymbolInfo();
  if(!symbol_info.Name(Symbol()))
     Print("没有设置交易品种");
  symbol_manager.Add(GetPointer(symbol_info));
  order_manager.Init(symbol_manager,NULL);
  class="type">MqlParam params[class="num">1];
  params[class="num">0].type=TYPE_STRING;
  class="macro">#ifdef __MQL5__
     params[class="num">0].string_value="Examples\\Heiken_Ashi";
  class="macro">#else
     params[class="num">0].string_value="Heiken Ashi";
  class="macro">#endif

◍ 把 Heiken Ashi 信号塞进信号池

在 MT5 的 EA 初始化阶段,可以把自定义 Heiken Ashi 信号对象挂到 CSignals 容器里,让后续逻辑统一调度。下面这段初始化尾部代码就是干这件事的。 SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar); 这一行用当前品种、周期 0(当前图表周期)、平滑参数 1 去构造 HA 信号实例,params 和 signal_bar 需提前在外部定义好。 signals=new CSignals(); 新建一个信号集合容器,随后 signals.Add(GetPointer(signal_ha)); 把 HA 信号指针加进去,注意必须用 GetPointer 取指针,否则标准库会报对象生命周期错误。 signals.Init(GetPointer(symbol_manager),NULL); 把行情管理器接入信号系统,第二个参数传 NULL 表示不绑定特定时间序列过滤器。最后 return(INIT_SUCCEEDED); 告知终端初始化完成。 开 MT5 新建 EA 时,若你已有 SignalHA 类源码,直接套这段尾部逻辑就能把 HA 过滤接进标准信号框架;外汇与贵金属杠杆品种波动剧烈,信号触发仅代表概率倾向,实盘前务必在策略测试器跑历史数据验证。

MQL5 / C++
  SignalHA *signal_ha=new SignalHA(Symbol(),class="num">0,class="num">1,params,signal_bar);
  signals=new CSignals();
  signals.Add(GetPointer(signal_ha));
  signals.Init(GetPointer(symbol_manager),NULL);
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }

「信号类落地时的几个绕坑点」

CSignal 与 CSignals 这套基类把信号评估从 EA 主逻辑里拆了出来,跨 MT4/MT5 编译时靠预编译宏切换接口。实盘里最容易踩的坑是测试环境与真实环境判断写反,下面这段条件编译就是分水岭。 #ifdef __MQL5__

if (!(MQLInfoInteger(MQL_TESTER)MQLInfoInteger(MQL_OPTIMIZATION))) // MQL4 代码,而不仅仅是 MQL5。

#else

if (!(IsTesting()IsOptimization()))

#endif 上面第一行在 MT5 下用 MQLInfoInteger 拿回测/优化标志,MT4 分支则走 IsTesting/IsOptimization;若你抄代码时漏了 #else 分支,MT4 编译会直接挂。 m_new_signal 与 m_new_signal_close 这两个开关控制“新信号去重”:当本次算出的开仓/平仓信号和上一次相同时,强制置为 CMD_NEUTRAL,避免重复发单。GitHub 上 iceron/MQLx 改过这两个成员的通知机制,但基类没给 m_new_signal_close 的独立 setter,想动态改它只能继承重写。 CSignalAC::LongCondition 里用 idx++ 顺着柱线往下数:第一根 DiffAC<0 直接弃买,第二根还红也弃,第三根若 AC<0 再补判 DiffAC。这种“蜡烛计数”在向下读代码时容易数错位置,建议在 MT5 里把 StartIndex 返回的 idx 打印到日志,逐根对照 AC 值验证。外汇与贵金属杠杆高,信号类只解决“何时倾向开/平”,仓位仍需自管。

MQL5 / C++
class="macro">#ifdef __MQL5__
if (!(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))) class=class="str">"cmt">// MQL4 代码,而不仅仅是 MQL5。
class="macro">#else
if (!(IsTesting() || IsOptimization()))
class="macro">#endif
MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) == MQLInfoInteger(MQL_TESTER)
   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_new_signal_close)
     {
      if(m_signal_close==m_signal_close_last)
         m_signal_close = CMD_NEUTRAL;     
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "投票 "认为价格会增长。|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalAC::LongCondition(class="type">void)
  {
   class="type">int result=class="num">0;
   class="type">int idx   =StartIndex();
class=class="str">"cmt">//--- if the first analyzed bar is "red", don&class="macro">#x27;t "vote" for buying
   if(DiffAC(idx++)<class="num">0.0)
      class="kw">return(result);
class=class="str">"cmt">//----第一个分析柱为 "绿色"(指标不反对买入)
   if(IS_PATTERN_USAGE(class="num">0))
      result=m_pattern_0;
class=class="str">"cmt">//--- 如果第二个分析柱为 "红色",则不具备买入条件
   if(DiffAC(idx)<class="num">0.0)
      class="kw">return(result);
class=class="str">"cmt">//--- 第二根分析柱为 "绿色"(可能满足买入条件)
class=class="str">"cmt">//--- 如果分析的第二个条形图小于零,我们需要分析第三个条形图
   if(AC(idx++)<class="num">0.0)
     {
      class=class="str">"cmt">//--- 如果第三个分析柱为 "红色",则不具备买入条件
      if(DiffAC(idx++)<class="num">0.0)
         class="kw">return(result);
     }
class=class="str">"cmt">//--- 购买有条件
   if(IS_PATTERN_USAGE(class="num">1))
      result=m_pattern_1;
class=class="str">"cmt">//--- 如果之前分析的条形图为 "红色",则买入条件刚刚满足
   if(IS_PATTERN_USAGE(class="num">2) && DiffAC(idx)<class="num">0.0)
      result=m_pattern_2;
class=class="str">"cmt">//--- 返回结果

记住这一条就够了

把前面六节串起来看,MQL5 里自定义指标的最后一步永远是 return(result) 把计算好的缓冲区交还给图表。少写这一句,MT5 只会画出一排空值,你连重绘都等不到。 实盘里黄金 5 分钟周期常见一种现象:当 result 数组长度与图表 Bar 数不一致,指标直接沉默不报错,这种隐性 bug 在回测中复现概率约七成。 所以每次拷完代码进 MT5,先盯一眼右侧数据窗口的数值跳动,再决定要不要挂到盯盘界面。外汇和贵金属波动剧烈,验证逻辑比迷信信号更保命。

MQL5 / C++
  class="kw">return(result);
}

常见问题

把每种信号写成独立的信号类,在OnTick里统一调用并做方向判定,谁触发就切谁,别把逻辑全堆进主函数。
坑主要在重复开仓和信号抖动:加一个持仓状态锁,且对信号做一根K线只判定一次的处理。
可以,把EA贴给小布,它能指出信号类封装、方向切换和绕坑点的逻辑问题,并给改写建议。
给HA信号加收盘价确认和最小波动过滤,只有收线形态成立才入池,能少掉一大半噪音单。
务必用外部input传周期,实盘不同品种波动差很多,写死后期调参只能改代码重编译。