为EA交易提供指标的现成模板(第2部分):交易量和比尔威廉姆斯指标·进阶篇
📊

为EA交易提供指标的现成模板(第2部分):交易量和比尔威廉姆斯指标·进阶篇

(2/3)· 6类量能指标与比尔威廉姆斯套装的句柄、析构与读数模板,复制即用

实战向进阶 第 2/3 篇

不少交易者把成交量指标直接拖进EA就以为能跑,结果句柄泄漏和周期错配让回测曲线失真。比尔威廉姆斯那套震荡与分形工具若每次手写初始化,调试时间远超策略本身。本文把这两类指标的模板拆成参数、初始化、析构、读数四步,接上篇仪表板直接显示。

「先把指标状态机与面板骨架搭起来」

做一套看盘面板,第一步不是画界面,而是把价格线相对参考值的状态先枚举清楚。上面这段代码定义了 ENUM_LINE_STATE,把「未定义 / 上行 / 下行 / 反转 / 停顿 / 上穿下穿 / 触碰 / 相等」等 14 种状态一次列全,后面判断只比对枚举值,不再写一堆大于小于。 注意它引入了 <Dashboard\Dashboard.mqh>,这是自绘面板的基类;全局里只留一个 CDashboard *panel 指针,具体控件在初始化时 new。鼠标所在 K 线的索引用 mouse_bar_index 单独存,和指标句柄 handle、小数位 ind_digits 分开,避免取数时串线。 输入参数只暴露了成交量类型 InpVolume,默认 VOLUME_TICK,说明这套逻辑后面要按 tick 量而非普通成交量跑。外汇与贵金属波动受消息影响大,tick _volume 在流动性突变时可能失真,接入后建议在 MT5 用 EURUSD 的 M5 先空载跑一遍,确认 panel 能跟随鼠标_bar 刷新再接信号。

MQL5 / C++
class=class="str">"cmt">//|                                                                  Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                             [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd."
class="macro">#class="kw">property link     "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//--- includes
class="macro">#include <Dashboard\Dashboard.mqh>
class=class="str">"cmt">//--- enums
enum ENUM_LINE_STATE
  {
   LINE_STATE_NONE,          class=class="str">"cmt">// Undefined state
   LINE_STATE_UP,            class=class="str">"cmt">// Upward
   LINE_STATE_DOWN,          class=class="str">"cmt">// Downward
   LINE_STATE_TURN_UP,       class=class="str">"cmt">// Upward reversal
   LINE_STATE_TURN_DOWN,     class=class="str">"cmt">// Downward reversal
   LINE_STATE_STOP_UP,       class=class="str">"cmt">// Upward stop
   LINE_STATE_STOP_DOWN,     class=class="str">"cmt">// Downward stop
   LINE_STATE_ABOVE,         class=class="str">"cmt">// Above value
   LINE_STATE_UNDER,         class=class="str">"cmt">// Below value
   LINE_STATE_CROSS_UP,      class=class="str">"cmt">// Crossing value upwards
   LINE_STATE_CROSS_DOWN,    class=class="str">"cmt">// Crossing value downwards
   LINE_STATE_TOUCH_BELOW,   class=class="str">"cmt">// Touching value from below 
   LINE_STATE_TOUCH_ABOVE,   class=class="str">"cmt">// Touch value from above
   LINE_STATE_EQUALS,        class=class="str">"cmt">// Equal to value
   };
class=class="str">"cmt">//--- input parameters
input ENUM_APPLIED_VOLUME  InpVolume  =  VOLUME_TICK;   class=class="str">"cmt">/* Applied Volume */
class=class="str">"cmt">//--- global variables
class="type">int      handle=INVALID_HANDLE;   class=class="str">"cmt">// Indicator handle
class="type">int      ind_digits=class="num">0;            class=class="str">"cmt">// Number of decimal places in the indicator values
class="type">class="kw">string   ind_title;               class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//--- variables for the panel
class="type">int      mouse_bar_index;         class=class="str">"cmt">// Index of the bar the data is taken from
CDashboard *panel=NULL;           class=class="str">"cmt">// Pointer to the panel object

EA 启动时的句柄与面板挂载

MT5 的 OnInit 只在加载 EA 时跑一次,这里要把全局变量和指标句柄先落定。先用 EventSetTimer(60) 挂一个 60 秒周期定时器,否则后续靠 OnTimer 轮询行情的写法根本不会触发。 指标侧先用 iAD(Symbol(),PERIOD_CURRENT,InpVolume) 拿累积/派发线(A/D)句柄,ind_title 设成 "A/D"、ind_digits 设 0,表示不保留小数。若返回 INVALID_HANDLE,说明创建失败,直接 return INIT_FAILED 并打错误码,EA 不会继续跑。 带仪表板的 EA 要在这里 new 一个 CDashboard 对象,构造参数 (1,20,20,199,225) 分别对应 ID、左上角 X/Y 和宽高。若返回 NULL 同样走 INIT_FAILED。 面板字体用 Calibri 9 号,View() 把 "品种, 周期" 写进表头;接着建两张表——ID 0 放 K 线数据,DrawGrid(0,2,20,6,2,18,97) 画出 2 列 6 行的栅格;ID 1 放指标数据,其 Y1 坐标取表 0 的 Y2 再加 22 像素,避免两块表格叠在一起。外汇与贵金属杠杆高,EA 初始化失败常导致漏单,上线前务必在策略测试器里跑一次 OnInit 验证逻辑。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title="A/D";
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAD(Symbol(),PERIOD_CURRENT,InpVolume);
   if(handle==INVALID_HANDLE)
     {
       PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError());
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Dashboard
class=class="str">"cmt">//--- Create the panel
   panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Set font parameters
   panel.SetFontParams("Calibri",class="num">9);
class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text
   panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7));
class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it
   panel.CreateNewTable(class="num">0);
class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background
   panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97);
class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it
   panel.CreateNewTable(class="num">1);
class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and
class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1
   class="type">int y1=panel.TableY2(class="num">0)+class="num">22;
class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background

◍ 面板网格与光标柱初始化怎么接

在指标或 EA 的初始化尾部,先用 DrawGrid 把面板网格画出来:参数依次是起始列 1、起始行 2、纵向坐标 y1,以及网格列数 3、行数 2、宽度 18、高度 97。这套数值决定面板在图表上的占位,改了宽高就要同步看 y1 是否还贴着价格轴。 网格画完别让它空着,用 GridPrint(0,2) 和 GridPrint(1,2) 把前两列的数据推到日志与面板。这里列索引从 0 起算,第二参数 2 代表打印到第 2 行,错位会导致数据显示在不可见区域。 接着把 mouse_bar_index 置 0,代表鼠标默认锚定在最新一根柱。最后调 DrawData(mouse_bar_index, TimeCurrent()) 把当前柱的实时数据刷到面板,并返回 INIT_SUCCEEDED 收尾。开 MT5 把这段接进 OnInitialize,能在初始化后立刻看到面板有内容而非空白。外汇与贵金属品种波动剧烈,面板数值仅作辅助参考,实盘前请在策略测试器验证稳定性。

MQL5 / C++
  panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97);
  class=class="str">"cmt">//--- Display tabular data in the journal
  panel.GridPrint(class="num">0,class="num">2);
  panel.GridPrint(class="num">1,class="num">2);
class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar
  mouse_bar_index=class="num">0;
class=class="str">"cmt">//--- Display the data of the current bar on the panel
  DrawData(mouse_bar_index,TimeCurrent());
class=class="str">"cmt">//--- Successful initialization
  class="kw">return(INIT_SUCCEEDED);
  }

「EA退出时的资源回收动作」

MT5 的 EA 在卸载或重编译时触发 OnDeinit(),这里是清理指标句柄和图形对象的最后关口。若句柄不释放,终端可能在后续加载时报 'indicator already loaded' 类错误,尤其在频繁改参数重编译的策略上概率更高。 下面这段是标准的析构写法:先杀定时器,再释放指标句柄并打印失败原因,最后清掉图表上的 Comment 文字。 //+------------------------------------------------------------------+

//Expert deinitialization function

//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); //--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); //--- Clear all comments on the chart Comment(""); } //+------------------------------------------------------------------+

//Expert deinitialization function

//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); //--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); //--- Clear all comments on the chart Comment(""); //--- If the panel object exists, delete it if(panel!=NULL) delete panel; } 逐行拆解:EventKillTimer() 停掉之前用 EventSetTimer 起的周期事件,避免退出了还回调;ResetLastError() 把上一次错误码清零,否则 GetLastError 可能读到脏值;IndicatorRelease(handle) 归还指标句柄,失败就打印函数名加错误码方便排错;Comment("") 抹掉右上角残留文字。 第二段多出的几行才是仪表板收尾关键:panel 是指针对象,不为 NULL 时直接 delete,由析构函数去销毁所有子控件。外汇与贵金属 EA 多为长周期挂机,退出不删面板会留下不可点击的幽灵对象,手动清图表很烦。开 MT5 把这段贴进 EA 测一遍,改个参数触发重编译就能看到面板随之消失。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- destroy timer
   EventKillTimer();
   
class=class="str">"cmt">//--- Release handle of the indicator
   ResetLastError();
   if(!IndicatorRelease(handle))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
class=class="str">"cmt">//--- Clear all comments on the chart
   Comment("");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- destroy timer
   EventKillTimer();
   
class=class="str">"cmt">//--- Release handle of the indicator
   ResetLastError();
   if(!IndicatorRelease(handle))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
class=class="str">"cmt">//--- Clear all comments on the chart
   Comment("");
   
class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it
   if(panel!=NULL)
      class="kw">delete panel;
  }

从指标句柄取数并判线态

把振荡指标接进 EA 后,真正干活的是两套取数函数:一套通用,一套给仪表板面板用。前者在早先关于振荡指标挂 EA 的文章里拆过,可直接拷进自定义程序当底层用。 面板场景里,取数函数负责把指标值和线状态刷到图上;同时 OnChartEvent() 里要调面板事件处理,并接住「光标下柱索引」这种鼠标交互事件,不然悬停读数会失灵。 编译跑起来后,你能在面板实时看到指标值和线段状态。随文给的 TestVolumeAD.mq5 是可直接加载的测试 EA,建议先拖欧元兑美元 M5 跑一遍,验证面板刷新延迟是否在 1~2 个 tick 内。 下面这段是取数与判态的核心:IndicatorValue 用 CopyBuffer 拉单根柱的某个 buffer,失败返 EMPTY_VALUE;LineState 则取 index、index+1、index+2 三根柱,靠相邻差值归一化后分类线段方向。外汇与贵金属波动大,这类线态判定在滑点行情中可能误触,实盘前务必用历史数据回测。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator data on the specified bar                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num)
  {
   class="type">class="kw">double array[class="num">1]={class="num">0};
   ResetLastError();
   if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1)
     {
      PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError());
      class="kw">return EMPTY_VALUE;
     }
   class="kw">return array[class="num">0];
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the indicator line                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num)
  {
class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index
   const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)

◍ 相对水平线的状态判定逻辑

在指标线状态识别里,除了相邻柱的走势判定,另一类是线相对于某个固定水平(比如布林中轨或自定义阈值)的位置关系。下面这段函数 LineStateRelative 接收指标句柄、索引、缓冲区编号以及一或两个水平值,先取 indexindex+1 两处数值,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。 它用 NormalizeDouble(...,ind_digits) 把浮点误差压到与品种小数位一致,再比较。线在水平下、上、上穿、下穿、从下/上触碰、等于,分别映射到 LINE_STATE_UNDER / ABOVE / CROSS_UP / CROSS_DOWN / TOUCH_BELOW / EQUALS 等枚举。注意原文有一处疑似笔误:从上方触碰的分支也返回了 LINE_STATE_TOUCH_BELOW,实盘前应改成 LINE_STATE_TOUCH_ABOVE 之类独立枚举,否则上下触碰会被混为一谈。 外汇与贵金属杠杆高、滑点随机,这类状态函数只描述概率性位置关系,不能当成反转或突破的确定性信号;建议在 MT5 里把 ind_digits 设成 _Digits 并接真实品种回测,观察触碰分支的触发频率再决定是否修正。

MQL5 / C++
ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE)
  {
class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index
   const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Define the second level to compare
   class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1);
class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0)
   if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_UNDER;
class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0)
   if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_ABOVE;
class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0)
   if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_CROSS_UP;
class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0)
   if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_CROSS_DOWN;
class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_TOUCH_BELOW;
class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_TOUCH_BELOW;
class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0)
   if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_EQUALS;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
把指标巡检交给小布盯盘
这些量能与比尔威廉姆斯指标的实时状态诊断,小布盯盘的AIGC已内置,打开对应品种页即可看到多周期背离提示,你只管判断信号真伪。

常见问题

外汇的成交量是该时间间隔内分时价格变动的次数,不是真实合约量;股票才是已执行交易的合约或货币量,两者加权含义不同。
集散指标A/D是平衡交易量的一个变体,都用成交量测算买卖压力,A/D对价格变动的加权更直接,模板里用iAD句柄即可。
可以,小布盯盘在品种页对鳄鱼、动量振荡等套装做多周期背离标注,省去手动比对指标与价格的功夫。
不释放会占用终端句柄额度,多次加载EA后可能达到上限导致新指标无法创建,回测或实盘都会静默失效。
分形依赖左右两根偏移确认,读取时需对齐当前周期与EA运行周期,避免用0误代具体ENUM_TIMEFRAMES造成信号延迟。