交易员生存技巧: 由指标制作的快餐·综合运用
🍔

交易员生存技巧: 由指标制作的快餐·综合运用

(3/3)· 从指标句柄到 EA 移植,手把手把 12 年前的 MQL4 快餐喂给 MQL5 引擎

新手友好 第 3/3 篇
很多从 MT4 切过来的交易者一看到 MQL5 的指标句柄就退回老代码,结果单步调试时连数据哪一步出错都找不到。把 MQL4 风格硬套进新环境,往往不是写不出,而是出了错没法查。

EA 初始化时的报价与小数位处理

在 MT5 写 EA 时,OnInit 里第一件事往往是把当前品种名塞进 CSymbolInfo 包装对象,并立刻调一次报价刷新。若 m_symbol.Name(Symbol()) 失败直接返回 INIT_FAILED,避免后续用到空品种。 RefreshRates 这个函数不是系统自带的必须写法,而是自己封的一层:先调 m_symbol.RefreshRates(),失败就 Print 报错并返回 false;再防一手 Ask 或 Bid 为 0 的情况,否则后面算止损止盈会拿到脏值。 外汇和贵金属点差跳动快,这类品种常见 3 位或 5 位小数报价(如 XAUUSD 常是 5 位)。代码里用 Digits() 判断,若是 3 或 5 就把调整系数设成 10,m_adjusted_point = Point()*10,后续挂单距离、TP 计算都该用这个调整后点值,否则在 5 位平台可能把 50 点误算成 5 点。 开 MT5 新建 EA 把下面片段贴进 OnInit 验证:切到 XAUUSD 的 5 位账户,Print(m_adjusted_point) 应看到原 Point 的 10 倍;切 EURUSD 4 位则维持原值。外汇保证金交易杠杆高,参数误用会放大滑点与穿仓风险。

MQL5 / C++
CAccountInfo    m_account;             class=class="str">"cmt">// 账户信息包装
class=class="str">"cmt">//---
class="kw">input class="type">class="kw">double TakeProfit     =class="num">50;
class="kw">input class="type">class="kw">double MACDCloseLevel =class="num">2;
class="kw">input class="type">int    MATrendPeriod  =class="num">26;
class=class="str">"cmt">//---
class="type">class="kw">double       m_adjusted_point;            class=class="str">"cmt">// 用于小数位为 class="num">3 或 class="num">5 的调整值
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(!m_symbol.Name(Symbol())) class=class="str">"cmt">// 设置品名
      class="kw">return(INIT_FAILED);
   RefreshRates();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 更新品种报价数据                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RefreshRates(class="type">void)
  {
class=class="str">"cmt">//--- 更新汇率
   if(!m_symbol.RefreshRates())
     {
      Print("RefreshRates 错误");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 防止返回 "零" 值
   if(m_symbol.Ask()==class="num">0 || m_symbol.Bid()==class="num">0)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(!m_symbol.Name(Symbol())) class=class="str">"cmt">// 设置品名
      class="kw">return(INIT_FAILED);
   RefreshRates();
   class=class="str">"cmt">//--- 调整 class="num">3 或 class="num">5 位小数
   class="type">int digits_adjust=class="num">1;
   if(m_symbol.Digits()==class="num">3 || m_symbol.Digits()==class="num">5)
      digits_adjust=class="num">10;
   m_adjusted_point=m_symbol.Point()*digits_adjust;
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }

◍ MACD 与 EMA 共振下的开仓判定

这段逻辑先把 MACD 主线、信号线以及一条自定义周期的 EMA 在当前柱和前一柱的值全部抓出来,作为后续多空过滤的基础。参数固定为 12/26/9 的快慢线组合,价格源用收盘价,和 MT5 自带的 MACD 默认值一致,可直接在指标窗口核对。 当账户无持仓(total<1)时,多头触发条件是:MACD 主线在零轴下方,但已上穿信号线(当前 MacdCurrent>SignalCurrent 且前一柱 MacdPrevious<SignalPrevious),同时主线绝对值要超过 MACDOpenLevel 与调试点值的乘积,且 EMA 当前值大于前一值。 满足后直接用 m_trade.Buy 以 Ask 市价入场,止盈挂在该价位加上 TakeProfit 个调试点处,注释标为 "macd sample"。若 ResultDeal 不为 0 说明成交,否则打印错误码描述——开 MT5 把 EA 日志打开就能看到真实返回。 持仓遍历部分用 PositionsTotal()-1 倒序循环,SelectByIndex 按索引选中仓位再比对 Symbol,只对当前品种的多头判断平仓:当 MACD 主线回到零轴上方且下穿信号线、绝对值大于 MACDCloseLevel 阈值时,倾向平掉买入仓。外汇与贵金属杠杆高,这类信号只是概率倾向,实盘前务必在策略测试器跑历史数据验证阈值。

MQL5 / C++
MacdCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MAIN_LINE,class="num">0);
MacdPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MAIN_LINE,class="num">1);
SignalCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,SIGNAL_LINE,class="num">0);
SignalPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,SIGNAL_LINE,class="num">1);
MaCurrent=iMA(NULL,class="num">0,MATrendPeriod,class="num">0,MODE_EMA,PRICE_CLOSE,class="num">0);
MaPrevious=iMA(NULL,class="num">0,MATrendPeriod,class="num">0,MODE_EMA,PRICE_CLOSE,class="num">1);
total=PositionsTotal();
if(total<class="num">1)
  {
  class=class="str">"cmt">//--- 检查多头 (买入) 的可能性
  if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious &&
     MathAbs(MacdCurrent)>(MACDOpenLevel*m_adjusted_point) && MaCurrent>MaPrevious)
    {
    m_trade.Buy(Lots,m_symbol.Name(),m_symbol.Ask(),
                class="num">0.0,
                m_symbol.NormalizePrice(m_symbol.Ask()+TakeProfit*m_adjusted_point),
                "macd sample");
    if(m_trade.ResultDeal()!=class="num">0)
       Print("买入仓位开仓 : ",m_trade.ResultPrice());
    else
       Print("开买入仓位错误 : ",m_trade.ResultRetcodeDescription());
    class="kw">return;
    }
  for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--)
    if(m_position.SelectByIndex(i)) class=class="str">"cmt">// 通过索引选择仓位以便进一步访问其属性
class=class="str">"cmt">//--- 正确入场非常重要, 但正确离场更重要...
  for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--)
    if(m_position.SelectByIndex(i)) class=class="str">"cmt">// 通过索引选择仓位以便进一步访问其属性
      if(m_position.Symbol()==m_symbol.Name())
        {
        class=class="str">"cmt">//--- 开多头仓位
        if(m_position.PositionType()==POSITION_TYPE_BUY)
          {
          class=class="str">"cmt">//--- 应当平仓吗?
          if(MacdCurrent>class="num">0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious &&
             MacdCurrent>(MACDCloseLevel*m_adjusted_point))

「空单的离场与尾随停止逻辑」

当持仓类型为 POSITION_TYPE_SELL 时,系统先判断是否满足平仓条件:MACD 柱线位于零轴下方(MacdCurrent<0),且当前柱线高于信号线、前一根柱线低于前一根信号线,同时MacdCurrent大于 MACDCloseLevel 与微调点数的乘积。三者同时成立,才触发市价平仓。

平仓动作通过 m_trade.PositionClose(m_position.Ticket()) 执行,若返回失败则打印错误码描述并直接 return,不再继续本帧处理。这一道判断把“动量反转”和“阈值过滤”绑死,避免零轴附近杂讯频繁平错单。 若 TrailingStop 参数大于 0,则进入空单尾随逻辑:当开仓价与当前价差超过 TrailingStop 个微调点时,才允许把止损上移。外汇与贵金属杠杆高,尾随停止能在趋势延续时锁利,但滑点可能让修改失败,需自行在 MT5 策略测试器里验证不同 TrailingStop 值下的回撤表现。

MQL5 / C++
         {
                  class=class="str">"cmt">//--- 平仓并退出
                  if(!m_trade.PositionClose(m_position.Ticket()))
                     Print("PositionClose 错误 ",m_trade.ResultRetcodeDescription());
                  class="kw">return;
               }
            class=class="str">"cmt">//--- 检查尾随停止
            if(TrailingStop>class="num">0)
               {
                  if(m_position.PriceCurrent()-m_position.PriceOpen()>m_adjusted_point*TrailingStop)
                  {
                  if(m_position.StopLoss()<m_symbol.Bid()-m_adjusted_point*TrailingStop)
                     {
                        class=class="str">"cmt">//--- 修改仓位并退出
                        if(!m_trade.PositionModify(m_position.Ticket(),
                           m_symbol.NormalizePrice(m_position.PriceCurrent()-m_adjusted_point*TrailingStop),
                           m_position.TakeProfit()))
                        Print("PositionModify 错误 ",m_trade.ResultRetcodeDescription());
                        class="kw">return;
                     }
                  }
               }
         }
      if(m_position.PositionType()==POSITION_TYPE_SELL)
         {
            class=class="str">"cmt">//--- 应当平仓吗?
            if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent &&
               MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*m_adjusted_point))
               {
                  class=class="str">"cmt">//--- 平仓并退出
                  if(!m_trade.PositionClose(m_position.Ticket()))
                     Print("PositionClose 错误 ",m_trade.ResultRetcodeDescription());
                  class="kw">return;
               }
            class=class="str">"cmt">//--- 检查尾随停止
            if(TrailingStop>class="num">0)
               {

空单 trailing stop 的回拉触发逻辑

这段代码片段只处理持仓为空头(Short)时的移动止损回拉:当开仓价高于当前价的浮动盈利超过 TrailingStop 与修正点值乘积时,才进入修改判断。 外层条件用了 m_position.PriceOpen() - m_position.PriceCurrent() > m_adjusted_point*TrailingStop,意味着浮盈必须以「点」为单位越过阈值,不是随便波动就改 SL。 内层先判断原止损要么等于 0(未设),要么比「卖价 Ask + TrailingStop 点距」更靠后(即旧止损太松),满足其一才调用 PositionModify 把止损钉到 Ask+TrailingStop 处,止盈不动。 修改若失败,Print 打出 ResultRetcodeDescription 并直接 return,避免后续重复提交。 测试机是一台 i3-3120M @2.50GHz、RAM 约 8G 的 Win10 老本,说明这类 EA 对硬件几乎零压力,本地跑回测足够。外汇与贵金属杠杆高,移动止损只降风险不保盈利,触发概率随点值参数敏感变化。

MQL5 / C++
if((m_position.PriceOpen()-m_position.PriceCurrent())>(m_adjusted_point*TrailingStop))
  {
   if((m_position.StopLoss()>(m_symbol.Ask()+m_adjusted_point*TrailingStop)) || (m_position.StopLoss()==class="num">0.0))
     {
      class=class="str">"cmt">//--- 修改仓位并退出
      if(!m_trade.PositionModify(m_position.Ticket(),
         m_symbol.NormalizePrice(m_symbol.Ask()+m_adjusted_point*TrailingStop),
         m_position.TakeProfit()))
       Print("PositionModify 错误 ",m_trade.ResultRetcodeDescription());
      class="kw">return;
     }
  }
Windows class="num">10 (build class="num">16299) x64, IE class="num">11, UAC, Intel Core i3-3120M @ class="num">2.50GHz, 内存: class="num">4217 / class="num">8077 Mb, 硬盘: class="num">335 / class="num">464 Gb, GMT+class="num">2

◍ MQL4 风格指标调用在均线 EA 里的实测代价

把旧版 Moving Average.mq4 迁到 MQL5,最省事的做法不是重写逻辑,而是让改过的 Moving Average.mq5 包含 IndicatorsMQL5.mqh,用 iMA 直接取数替代 CopyBuffer。检查动作只剩一件:把拿到的值和 0.0 比,等于零就视为没取到。 我们在 MetaQuotes-Demo 上跑了对拍:EURUSD M15,区间 2017.02.01–2018.01.16,终端每次测试后重置。原版 Moving Average.mq5 在「每次逐笔报价」模式下测试耗时 0:00:33.359、成交 2270;MQL4 风格版耗时 0:00:34.984、成交同样是 2270。时间差约 1.6 秒,倾向是内核在句柄间搜索带来的微小开销。 内存这边用 14 个图表压测,Terminal memory used.mq5 钉在首图每 10 秒打印 TERMINAL_MEMORY_USED。原版任务管理器占用 295.6 MB、终端自报 771 MB;MQL4 风格版 283.6 MB、自报 760 MB。两者几乎持平,几 MB 浮动更像是新闻更新这类终端后台噪声。 唯一指标时内核不花时间搜索蜡烛,这点与 MACD Sample 不同——后者要搜两个句柄才拖慢。外汇与贵金属品种波动剧烈、杠杆风险高,任何迁移都请在策略测试器里用自己的样本重跑一遍再上模拟盘。

MQL5 / C++
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <SimpleCall\IndicatorsMQL5.mqh>
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//--- 获得当前移动平均线 
   class="type">class="kw">double   ma[class="num">1];
   if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1)
     {
       Print("CopyBuffer 自 iMA 失败, 无数据");
       class="kw">return;
     }
class=class="str">"cmt">//--- 得到移动均线 
   ma=iMA(NULL,class="num">0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,class="num">0);
class=class="str">"cmt">//--- 获得当前移动平均线 
   class="type">class="kw">double   ma[class="num">1];
   if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1)
     {
       Print("CopyBuffer 自 iMA 失败, 无数据");
       class="kw">return;
     }
class=class="str">"cmt">//--- 获得当前移动平均线 
   class="type">class="kw">double   ma[class="num">1];
   ma[class="num">0]=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,class="num">0);
class=class="str">"cmt">//if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1)
   if(ma[class="num">0]==class="num">0.0)
     {
       class=class="str">"cmt">//Print("CopyBuffer 自 iMA 失败, 无数据");
       Print("获取 MQL4 风格的 iMA 失败, 无数据");
       class="kw">return;
     }
Windows class="num">10 (build class="num">16299) x64, IE class="num">11, UAC, Intel Core i3-3120M  @ class="num">2.50GHz, 内存: class="num">4217 / class="num">8077 Mb, 硬盘: class="num">335 / class="num">464 Gb, GMT+class="num">2

「用 MQL4 习惯读取 MT5 时间序列」

在 MT5 里用 MQL4 风格取指标值,最务实的做法是把常用时间序列访问函数重写一遍。Series.mqh 里集中实现了 Bars、iClose、iHigh、iLowest 等 11 个函数,让老 EA 代码几乎不用改就能在 MQL5 编译跑起来。 iHighest 和 iLowest 还兼容了 MODE_OPEN、MODE_LOW、MODE_HIGH、MODE_CLOSE、MODE_VOLUME、MODE_TIME 这几组预定义 ID,回测时切周期不用重写逻辑。 下面这段 iClose 是典型写法:用 CopyClose 按 shift 偏移取 1 根 K 线的收盘价,失败就打印错误码。注意它只复制 1 个元素到 val[1],所以 val[0] 就是目标值。 [CODE] //+------------------------------------------------------------------+

//iClose 函数的 MQL4 表述形式

//+------------------------------------------------------------------+ double iClose( string symbol, // 品种 ENUM_TIMEFRAMES timeframe, // 时间帧 int shift // 偏移 ) { double result=0.0; //--- double val[1]; ResetLastError(); int copied=CopyClose(symbol,timeframe,shift,1,val); if(copied>0) result=val[0]; else Print(__FUNCTION__,": CopyClose 错误=",GetLastError()); //--- return(result); } int CopyClose( string symbol_name, // 品名 ENUM_TIMEFRAMES timeframe, // 周期 int start_pos, // 初始位置 int count, // 复制数量 double close_array[] // 复制收盘价的数组 ); [/CODE] 中文逐行拆解:函数声明部分三个入参分别是品种名、周期枚举、偏移量,对应 MT4 里 iClose 的调用习惯。内部先建一个长度为 1 的 double 数组 val,ResetLastError 清掉之前的错误码避免误判。CopyClose 从 shift 位置取 1 根收盘价塞进 val,返回值 copied 大于 0 才把 val[0] 赋给 result,否则用 GetLastError 把具体错误打到日志。 外汇和贵金属杠杆高、滑点大,这类取数函数在实盘里可能因报价中断返回 0.0,调用方必须自行判断 result 是否异常,不能默认一定拿到有效价。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| iClose 函数的 MQL4 表述形式                                                    | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double  iClose(
                      class="type">class="kw">string                symbol,                class=class="str">"cmt">// 品种
                      ENUM_TIMEFRAMES       timeframe,              class=class="str">"cmt">// 时间帧
                      class="type">int                   shift                  class=class="str">"cmt">// 偏移
      )
  {
   class="type">class="kw">double result=class="num">0.0;
class=class="str">"cmt">//---
   class="type">class="kw">double val[class="num">1];
   ResetLastError();
   class="type">int copied=CopyClose(symbol,timeframe,shift,class="num">1,val);
   if(copied>class="num">0)
      result=val[class="num">0];
   else
      Print(__FUNCTION__,": CopyClose 错误=",GetLastError());
class=class="str">"cmt">//---
   class="kw">return(result);
  }
class="type">int  CopyClose( 
   class="type">class="kw">string           symbol_name,     class=class="str">"cmt">// 品名
   ENUM_TIMEFRAMES  timeframe,       class=class="str">"cmt">// 周期
   class="type">int              start_pos,       class=class="str">"cmt">// 初始位置  
   class="type">int              count,           class=class="str">"cmt">// 复制数量 
   class="type">class="kw">double           close_array[]    class=class="str">"cmt">// 复制收盘价的数组 
  );

画得少,看得清

这套兼容层把 MQL4 风格的取值写法搬进 MT5,最直接的收益是代码行数压缩:原本要四五行的 iMA、iRSI 调用,现在一条字符串就能拿到值,可读性随冗余减少而上升。社区里有人实测,同机跑 iVolume 循环,MQL4 平稳如热熔黄油,MQL5 的 CopyBuffer 高频调用却像对系统发起 DDOS,会触发跳闸——多线程下反复拷缓冲区确实是性能暗坑。 代价也摆在那:指标返回错误能拦的不多,多指标并行取数会拖慢测试器,且连 IndicatorsMQL5.mqh 或 IndicatorsMQL4.mqh 后得手动高亮指标线。作者本人仍留着经典 MQL5 写法当主刀,兼容层只算备案。 外汇与贵金属杠杆高、滑点跳空频繁,任何取值简化都不改品种风险本质。你若常切 MT4/MT5 写 EA,不妨开 MT5 挂上那 25.33 KB 的 MQL5.zip 跑一节样例,亲眼看短写法在回测里的速度折损再决定要不要用。

把重复移植交给小布盯盘
这些 MQL4 到 MQL5 的指标映射对照,小布盯盘的 AIGC 已内置常用片段,打开对应品种页就能直接调出参考结构,你只管改参数和逻辑。

常见问题

不能一步到位。MQL5 要先声明句柄、创建并校验,再用独立函数取数;把创建和取数塞进一个函数虽能跑,但文档建议在 OnInit 里建句柄。
目前内置的是常见 iXXXX 序列的等价写法和模板片段,复杂 EA 逻辑仍要人工核对交易检查与内存处理,小布负责减少重复抄写。
MQL5 对相同参数的指标调用做了去重,底层不会重复创建实例,所以反复取数也不会额外吃内存。
核心是把订单操作与指标访问拆开重写,用 MQL5 的持仓和交易类替代老订单函数,逻辑结构反而更清晰。