交易员生存技巧: 由指标制作的快餐·进阶篇
📘

交易员生存技巧: 由指标制作的快餐·进阶篇

第 2/3 篇

把 MACD 封装成一行取数函数

在 MT5 里直接反复调 iMACD 句柄很啰嗦,老手通常把它包一层:传入品种、周期和三个均线周期,返回指定 buffer 与 shift 的当前值。下面这段就是一个典型的 MQL5 封装,把指标句柄、CopyBuffer 和越界判断全塞进一个 double 函数里。 代码里 fast_ema_period、slow_ema_period、signal_period 分别对应快线、慢线和信号线周期,buffer 参数决定取主图柱还是信号线(0 通常是 MACD 主线,1 是信号线)。shift=0 拿最新一根,shift=1 就是前一根——回测里常靠这个比对穿越。 外汇与贵金属杠杆高,这类取数函数只是工具,信号真假仍要看上下文,用错周期可能频繁假突破。开 MT5 把这段贴进脚本,改 symbol 为 XAUUSD 跑一下,能直观看到不同 shift 下的柱值差异。

MQL5 / C++
class="type">class="kw">double iMACD(
                      class="type">class="kw">string               symbol,               class=class="str">"cmt">// 品名 
                      ENUM_TIMEFRAMES      period,               class=class="str">"cmt">// 周期 
                      class="type">int                  fast_ema_period,      class=class="str">"cmt">// 快速均线计算周期 
                      class="type">int                  slow_ema_period,      class=class="str">"cmt">// 慢速均线计算周期 
                      class="type">int                  signal_period,        class=class="str">"cmt">// 差值均化周期 
                      ENUM_APPLIED_PRICE   applied_price,        class=class="str">"cmt">// 处理价格 
                      class="type">int                  buffer,               class=class="str">"cmt">// 缓存区
                      class="type">int                  shift                 class=class="str">"cmt">// 偏移
                      )
  {
   class="type">class="kw">double result=NULL;
class=class="str">"cmt">//---
   class="type">int handle=iMACD(symbol,period,fast_ema_period,slow_ema_period,signal_period,
                    applied_price);
   class="type">class="kw">double val[class="num">1];
   class="type">int copied=CopyBuffer(handle,buffer,shift,class="num">1,val);
   if(copied>class="num">0)
      result=val[class="num">0];
   class="kw">return(result);
  }

「把 iAC 包成 MQL4 式取数函数」

在 MT5 里直接写 iAC(symbol, timeframe, shift) 这种 MQL4 风格调用是编不过的,得先拿指标句柄再 CopyBuffer。下面这段把 Accelerator Oscillator 封装成旧版语法,方便从 MQL4 迁移过来的老代码少改几行。 关键点在 NaN 的处理:用 double NaN=double("nan") 造一个安静的缺失值,函数开头 result 先等于它,句柄无效或拷贝失败就原样返回,不会让调用方拿到 0 误判成真实信号。外汇与贵金属波动大,这种缺失标记能避免脏数据进策略。 OnTick 里另有一行 iMACD 取主缓存索引 1,即倒数第二根 BAR 的 MACD 值(12/26/9 参数,收盘价),用 Comment 打到图表左上。开 MT5 把下面代码贴进 EA,切到 XAUUSD 的 M5,能直接看见 MACD 主线前一柱数值随 tick 刷新。

MQL5 / C++
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="type">class="kw">double macd_main_1=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MAIN_LINE,class="num">1);
   Comment("MACD, 主缓存区, 索引 class="num">1: ",DoubleToString(macd_main_1,Digits()+class="num">1));
  }
class="type">class="kw">double NaN=class="type">class="kw">double("nan");
class="type">class="kw">double  iAC(
                        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=NaN;
class=class="str">"cmt">//---
   class="type">int handle=iAC(symbol,timeframe);
   if(handle==INVALID_HANDLE)
     {
      Print(__FUNCTION__,": INVALID_HANDLE 错误=",GetLastError());
      class="kw">return(result);
     }
   class="type">class="kw">double val[class="num">1];
   class="type">int copied=CopyBuffer(handle,class="num">0,shift,class="num">1,val);
   if(copied>class="num">0)
      result=val[class="num">0];
   else
      Print(__FUNCTION__,": CopyBuffer 错误=",GetLastError());
   class="kw">return(result);
  }
class="type">void OnStart()
  {

◍ NaN 比较陷阱与布林通道缓冲区编号差异

在 MQL5 里用 double("nan") 造出的 NaN,跟任何有限数或 0 做大小、相等比较都返回 false。下面这段最小示例直接在 MT5 跑,打印结果全是 false:NaN>10.3=false、NaN<-5=false、(NaN==0)=false、(NaN==NaN)=false。 这意味着你若用 if(price==EMPTY_VALUE)if(val==val) 去筛指标空值,逻辑会直接失效;正确做法是用 MathIsValidNumber() 或显式判断 val!=val 来捕获 NaN。外汇与贵金属行情在跳空或离线计算时可能产生这类脏值,属于高风险数据异常,务必在 EA 初始化阶段兜底。 顺带看指标缓冲区编号:MQL4 的 iBands 用 0-MODE_MAIN、1-MODE_UPPER、2-MODE_LOWER;MQL5 对应 0-BASE_LINE、1-UPPER_BAND、2-LOWER_BAND,映射关系整齐。但 iEnvelopes 在 MQL5 里变成了 0-UPPER_LINE、1-LOWER_LINE,没有中线缓冲,老代码迁移时若硬套 MQL4 的 0 号为主线会读错数据。

MQL5 / C++
class="type">class="kw">double NaN=class="type">class="kw">double("nan");
  class="type">class="kw">double a=class="num">10.3;
  class="type">class="kw">double b=-class="num">5;
  class="type">class="kw">double otherNaN=class="type">class="kw">double("nan");
  Print("NaN>class="num">10.3=",NaN>a);
  Print("NaN<-class="num">5=",NaN<b);
  Print("(NaN==class="num">0)=",NaN==class="num">0);
  Print("(NaN==NaN)=",NaN==otherNaN);
class=class="str">"cmt">//--- 结果
  NaN>class="num">10.3=class="kw">false
  NaN<-class="num">5=class="kw">false
  (NaN==class="num">0)=class="kw">false
  (NaN==NaN)=class="kw">false

逐笔报价下两种 MACD 调用的内存账本

把终端最大柱线锁在 100000,开 14 个图表:13 个挂 EA,第 14 个跑 Terminal memory used.mq5,每 10 秒打印一次 TERMINAL_MEMORY_USED,启动后静置不动、不切页面。 对比 iMACd(正规 iMACD 调用)与 MACD MQL4 style EA short(MQL4 风格逐笔调用),初始任务管理器占用分别为 279.7 MB 与 279.9 MB,TERMINAL_MEMORY_USED 都是 745 MB;静跑 10 分钟两者几乎零增长,说明单纯挂着不切周期时,两种写法内存表现一致。 把 13 个图表从 M30 切到 H1 后再看:iMACd 的任务管理器占用跳到 398.0 MB(10 分钟后 398.3 MB),而 MQL4 风格 EA 只到 319.2 MB(10 分钟后 330.5 MB)。TERMINAL_MEMORY_USED 两边分别为 869 与 874 MB。 有意思的是,切高周期后正规 iMACD 反而比 MQL4 风格多吃约 79 MB 物理内存,但终端自报标识符两者只差 5 MB。外汇与贵金属品种多图同跑时,这种隐性开销差异值得在 MT5 任务管理器里自己复测一遍。

「把 MQL4 版 MACD EA 搬进 MQL5 后的速度与内存账」

把经典 MACD Sample.mq4 以最小改动移植到 MQL5,需要包含 IndicatorsMQL4.mqh 以及 CPositionInfo、CTrade、CSymbolInfo、CAccountInfo 等交易类,并补一组指标线名称的 define 模块,才能用 MQL4 风格去抓指标值和下单。 为了兼容三位或五位报价小数,要引入一个 m_adjusted_point 乘数;在 OnInit 里初始化 m_symbol 对象后顺手算好它,OnTick 中调用 RefreshRates() 确认报价非 0.0,再用 NormalizePrice 把开仓价规整到最小变动位。 原版 MQL5 的 MACD Sample 一次从 CopyBuffer 拉两个值(数组维度 2),而 MQL4 风格版主线、信号线、EMA 各取两次单值。我们做了三个对照 EA:标准版、单次取值版、MQL4 风格移植版。 在 MetaQuotes-Demo 上跑 USDJPY M30,区间 2017.02.01–2018.01.16,PC 每次测试后重置。逐笔真实报价模式下标准版耗时 0:01:19.485,单次取值版 0:01:20.344,MQL4 风格版 0:02:37.422——后者比正确访问指标的同类慢约两倍。 内存侧用 Terminal memory used.mq5 每 10 秒打一次 TERMINAL_MEMORY_USED,14 图表环境里标准版 813 MB、单次版 813 MB、MQL4 风格版 818 MB。结论很直接:MQL4 风格移植在内存上几乎无差别,代价全压在执行耗时上。外汇与贵金属交易本身高风险,这类回测只反映历史环境,实盘性能可能随经纪商服务器漂移。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果处理过任何仓位, 主函数返回 true                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSampleExpert::Processing(class="type">void)
  {
class=class="str">"cmt">//--- 更新汇率
   if(!m_symbol.RefreshRates())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 更新指标
   if(BarsCalculated(m_handle_macd)<class="num">2 || BarsCalculated(m_handle_ema)<class="num">2)
      class="kw">return(class="kw">false);
   if(CopyBuffer(m_handle_macd,class="num">0,class="num">0,class="num">2,m_buff_MACD_main)   !=class="num">2 ||
      CopyBuffer(m_handle_macd,class="num">1,class="num">0,class="num">2,m_buff_MACD_signal)!=class="num">2 ||
      CopyBuffer(m_handle_ema,class="num">0,class="num">0,class="num">2,m_buff_EMA)          !=class="num">2)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//   m_indicators.Refresh();
class=class="str">"cmt">//--- 简化编码并加速访问
class=class="str">"cmt">//--- 数据被放入内部变量
   m_macd_current   =m_buff_MACD_main[class="num">0];
   m_macd_previous  =m_buff_MACD_main[class="num">1];
   m_signal_current =m_buff_MACD_signal[class="num">0];
   m_signal_previous=m_buff_MACD_signal[class="num">1];
   m_ema_current    =m_buff_EMA[class="num">0];
   m_ema_previous   =m_buff_EMA[class="num">1];
class=class="str">"cmt">//--- 简化编码并加快访问放入内部变量的数据

◍ 把 MQL4 习惯搬进 MQL5 的指标读取

老脚本党刚切到 MT5 最容易卡在指标调用上。上面这段用 iMACD / iMA 直接取柱值,就是典型的 MQL4 写法:12/26/9 参数的 MACD 主线和信号线,加上一条自定义周期的 EMA,各取当前与上一根两根柱,够做交叉判断用。 真正跑起来时,用 BarsCalculated 先确认两个句柄都至少有 2 根数据,否则直接 return(false) 避免空缓冲崩 EA。注释掉的那几行是早期 CopyBuffer 批量拷 2 根的做法,后来改成每次只拷 1 根进内部变量,访问更快、代码更短。 底部那串 #include 才是 MQL5 正统路数:PositionInfo、Trade、SymbolInfo、AccountInfo 四个对象把仓位和品种信息封装好。若你手头有 MQL4 老策略想重写,挂上 IndicatorsMQL4.mqh 并用 MODE_MAIN / MODE_SIGNAL 宏定义,基本能零改动移植指标读取逻辑。外汇与贵金属杠杆高,回测通过不代表实盘稳,上 MT5 前先开模拟盘验证信号触发节奏。

MQL5 / C++
  MacdCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MODE_MAIN,class="num">0);
  MacdPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MODE_MAIN,class="num">1);
  SignalCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MODE_SIGNAL,class="num">0);
  SignalPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MODE_SIGNAL,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);
class=class="str">"cmt">//--- 更新指标
  if(BarsCalculated(m_handle_macd)<class="num">2 || BarsCalculated(m_handle_ema)<class="num">2)
     class="kw">return(class="kw">false);
class=class="str">"cmt">//   if(CopyBuffer(m_handle_macd,class="num">0,class="num">0,class="num">2,m_buff_MACD_main)   !=class="num">2 ||
class=class="str">"cmt">//       CopyBuffer(m_handle_macd,class="num">1,class="num">0,class="num">2,m_buff_MACD_signal)!=class="num">2 ||
class=class="str">"cmt">//       CopyBuffer(m_handle_ema,class="num">0,class="num">0,class="num">2,m_buff_EMA)            !=class="num">2)
class=class="str">"cmt">//       class="kw">return(class="kw">false);
class=class="str">"cmt">//    m_indicators.Refresh();
class=class="str">"cmt">//--- 简化编码并加速访问
class=class="str">"cmt">//--- 数据被放入内部变量
   CopyBuffer(m_handle_macd,class="num">0,class="num">0,class="num">1,m_buff_MACD_main);
   m_macd_current=m_buff_MACD_main[class="num">0];
   CopyBuffer(m_handle_macd,class="num">0,class="num">1,class="num">1,m_buff_MACD_main);
   m_macd_previous=m_buff_MACD_main[class="num">0];
   CopyBuffer(m_handle_macd,class="num">1,class="num">0,class="num">1,m_buff_MACD_signal);
   m_signal_current=m_buff_MACD_signal[class="num">0];
   CopyBuffer(m_handle_macd,class="num">1,class="num">1,class="num">1,m_buff_MACD_signal);
   m_signal_previous=m_buff_MACD_signal[class="num">0];
   CopyBuffer(m_handle_ema,class="num">0,class="num">0,class="num">1,m_buff_EMA);
   m_ema_current=m_buff_EMA[class="num">0];
   CopyBuffer(m_handle_ema,class="num">0,class="num">1,class="num">1,m_buff_EMA);
   m_ema_previous=m_buff_EMA[class="num">0];
class="macro">#class="kw">property description "以 MQL4 的风格访问指标"
class="macro">#define MODE_MAIN     class="num">0
class="macro">#define MODE_SIGNAL   class="num">1
class="macro">#include <SimpleCall\IndicatorsMQL4.mqh>
class=class="str">"cmt">//---
class="macro">#include <Trade\PositionInfo.mqh>
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trade\SymbolInfo.mqh>  
class="macro">#include <Trade\AccountInfo.mqh>
CPositionInfo  m_position;                    class=class="str">"cmt">// 交易仓位对象
CTrade         m_trade;                       class=class="str">"cmt">// 交易对象
CSymbolInfo    m_symbol;                      class=class="str">"cmt">// 品名信息对象

常见问题

封装一个带默认参数的自定义函数,内部调用内置指标句柄并读取指定缓冲,外部调用时只传品种、周期和偏移量即可。
布林通道缓冲区编号与常见指标不同,且空值未必是 NaN,应先判断指标句柄和复制返回数,别只靠 NaN 比较。
可以,小布能对比你的取数代码在逐笔报价下的句柄创建与释放记录,标出重复创建和未释放的地方。
复用同一指标句柄并只读取需要的缓冲更省内存,每次重新创建句柄会在高频tick下累积占用。
多数卡在频繁创建/销毁句柄和错误使用缓冲区索引,统一用句柄缓存和正确编号能明显提速。