跨平台的EA交易: 信号·综合运用
把信号逻辑塞进类里:四个跨平台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 才能验证。
<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<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) && !last.IsClosed()) order_manager.CloseOrder(last); <span class="keyword">if</span> (direction<=<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 数据回测验证滑点下的表现。
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,说明后续还能挂自定义指标信号,但本节没给实现体,别指望直接编译通过。
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 是否生效。
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 ¶ms[],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 ¶ms[],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 ¶ms[],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 过滤接进标准信号框架;外汇与贵金属杠杆品种波动剧烈,信号触发仅代表概率倾向,实盘前务必在策略测试器跑历史数据验证。
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 值验证。外汇与贵金属杠杆高,信号类只解决“何时倾向开/平”,仓位仍需自管。
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,先盯一眼右侧数据窗口的数值跳动,再决定要不要挂到盯盘界面。外汇和贵金属波动剧烈,验证逻辑比迷信信号更保命。
class="kw">return(result);
}