class="kw">string LineStateDesc""> class="kw">string LineStateDesc"">
为EA交易提供指标的现成模板(第2部分):交易量和比尔威廉姆斯指标·综合运用
📘

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

第 3/3 篇
本章目录
  1. 把线段状态映射成可读字符串
  2. 把K线字段塞进面板表格
  3. 把光标位置喂给面板做实时读数
  4. 用 MFI 抓资金背离与极端区
  5. iMFI 句柄与 EA 输入参数的落地写法
  6. RSI 超卖阈值与线态枚举的落地写法
  7. RSI面板的全局变量与输入口
  8. EA 启动时的句柄与面板挂载
  9. 在面板上叠放两张数据网格
  10. EA卸载时句柄与面板对象的清理
  11. 从指标句柄抓数并判线态
  12. 指标线与阈值级别的六种相对状态判定
  13. 把指标线状态映射成可读文本
  14. 把当前K线塞进自定义面板表格
  15. 面板里把超买超卖状态画出来
  16. 把鼠标坐标变成可读的K线数据
  17. 用 OBV 读聪明钱的进出节奏
  18. OBV 句柄的参数与声明方式
  19. 给指标状态机先铺好枚举与面板骨架
  20. EA 启动时要先绑好指标句柄和面板
  21. 面板网格与光标柱数据的初始化衔接
  22. EA退出时把指标句柄和面板清干净
  23. 用句柄把指标值搬进面板
  24. 指标线与关卡位置的六种状态判定
  25. 把线态枚举翻译成可读标签
  26. 把K线字段塞进面板表格的画法
  27. 把光标位置喂给面板做实时回显
  28. iVolumes 句柄的参数与 EA 声明方式
  29. 正文
  30. EA 启动时的句柄与面板挂载
  31. 网格绘制与光标索引的初始接线
  32. EA卸载时别忘了释放指标句柄
  33. 用句柄把指标值搬进面板
  34. 指标线与参考位的相对状态判定
  35. 把指标线状态映射成可读文本
  36. 把 K 线字段铺进面板表格
  37. 把光标下的指标状态画进面板
  38. 面板关闭事件与比尔威廉姆斯指标分组
  39. AC 指标怎么提前半步抓转向
  40. 加速振荡指标的句柄与参数声明
  41. 给指标状态机先铺好枚举与句柄
  42. EA 启动时的指标句柄与面板挂载
  43. 面板网格与光标柱初始化衔接
  44. EA退出时如何干净释放指标与面板
  45. 指标句柄取数与面板着色逻辑
  46. 指标线与参考位的相对状态判定
  47. 把线状态枚举翻成可读文本
  48. 把当前K线字段铺到面板表格里
  49. 鼠标悬停读出光标处指标状态
  50. 面板关闭按钮的事件回调怎么接
  51. 用鳄鱼线分辨盲鸡行情
  52. 参数
  53. 鳄鱼指标的参数与句柄初始化
  54. 鳄鱼三线的状态枚举与输入参数拆解
  55. 面板与唇线变量的初始化清单
  56. EA 启动时的句柄与面板挂载
  57. 面板表格的布局与初始化绘制
  58. EA退出时把指标句柄和面板清干净
  59. 从指标句柄抓数据的两套函数
  60. 指标线与参考位的相对状态判定
  61. 把指标线状态翻译成人话
  62. 在自定义面板上铺开 K 线明细
  63. 把鳄鱼三线状态画进自定义面板
  64. 把鳄鱼唇线与齿线的相对状态画到面板上
  65. 面板关闭按钮的事件落点
  66. AO 碟形与零轴交叉的触发条件
  67. AO指标句柄的声明与参数接口
  68. 正文
  69. EA 启动时的指标句柄与面板挂载
  70. 把网格数据刷进日志并锁定光标柱
  71. EA退出时把指标句柄和面板清干净
  72. 从句柄抓数与零线状态判定
  73. 指标线与指定水平的相对状态判定
  74. 把指标线状态映射成可读字符串
  75. 把 K 线字段塞进面板表格的写法
  76. 在面板里把 AO 状态画出来
  77. 把鼠标位置翻译成K线索引
  78. 用分形抓顶底,但先过鳄鱼那关
  79. 分形指标句柄的入参与内存回收
  80. EA 启动时的句柄与面板挂载
  81. 面板网格与光标柱的初始化接驳
  82. EA退出时别忘了释放指标句柄
  83. 从指标句柄抓缓冲值的落地写法
  84. 用枚举把指标线相对价位的状态拆干净
  85. 把线态枚举翻成可读字符串
  86. 把当前K线塞进面板表格
  87. 鼠标悬停实时回写分形数值
  88. 用双直方图读鳄鱼线的咬合与张口
  89. 参数
  90. 鳄鱼线状态枚举与输入参数拆解
  91. 鳄鱼指标状态枚举与参数骨架
  92. 面板与均线周期的变量落地
  93. EA 启动时的句柄与面板落地
  94. 面板双表格的绘制与初始数据填充
  95. EA卸载时句柄与面板对象的清理
  96. 面板怎么把指标数值读出来
  97. 指标线与参考位的相对状态判定
  98. 把线态枚举翻成可读文字
  99. 在自定义面板上打印K线字段与指标值
  100. 把双直方图指标塞进自定义面板
  101. 把鼠标所在 K 线塞进全局变量
  102. BW MFI 与量能的四象限读法
  103. BWMFI 句柄与参数怎么挂进 EA
  104. 给指标状态机配上面板指针
  105. EA 启动时的句柄与面板装配
  106. 把网格数据刷进日志并锁定光标柱
  107. EA退出时别忘了释放指标与面板
  108. 从指标句柄抓数与线态判定
  109. 指标线与指定水平的相对状态判定
  110. 把指标状态翻译成人话的函数
  111. 把当前K线数据塞进面板表格
  112. 把 MFI 与成交量状态叠进面板单元格
  113. 把鼠标位置翻译成 K 线索引
  114. 面板表格坐标与折叠状态的持久化修复
  115. 给表格框定坐标与尺寸的取值器
  116. 表格行与单元格的计数逻辑
  117. 仪表盘类的私有成员与构造拆解
  118. 面板字体与表格绘制的接口拆解
  119. 取表格单元格坐标的封装写法
  120. 表格坐标与单元格的取值兜底
  121. 面板坐标与像素存档的变量命名
  122. 析构时如何保住面板状态
  123. 面板收起与文字绘制的内存账本
  124. 仪表盘里的文本擦写与表格登记逻辑
  125. 按名称取表对象与网格底图绘制
  126. 表格坐标与网格线的绘制逻辑
  127. 网格自动填充时的单元格坐标落点
  128. 画表格网格时顺手把行对象建好
  129. 把表格行渲染进画布并落盘像素
  130. 像素数组的落盘与回读容错
  131. 别急着下结论

把线段状态映射成可读字符串

做价格行为面板时,最怕状态枚举直接打印成数字,肉眼根本分不清当前是上破还是回踩。下面这个函数把 ENUM_LINE_STATE 的 13 种状态逐一转成英文短词,方便往 GUI 表格里写。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP         :  class="kw">return "Up";
      case LINE_STATE_STOP_UP    :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP    :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN       :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN  :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN  :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE      :  class="kw">return "Above level";
      case LINE_STATE_UNDER      :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP   :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN :  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS     :  class="kw">return "Equals";
      class="kw">default                    :  class="kw">return "Unknown";
     }
  }
逐行看:函数入参是 const 枚举,保证不会被误改;switch 里每个 case 对应一种线段关系,比如 LINE_STATE_TOUCH_BELOW 是价格从下方触碰线、LINE_STATE_CROSS_DOWN 是向下穿越。default 兜底返回 Unknown,避免面板出现空白格。 实际接数据时用另一个函数抓盘口。DrawData 先通过 SymbolInfoTick 取最新 tick,若失败直接 return;再用 CopyRates 按 index 取 1 根 Bar,返回值不等于 1 也退出。这两道校验能挡掉大部分离线回测或品种停盘时的崩溃。 字体部分把表头设成 9 号加粗,Bar 数据用暗红、Indicator 数据用绿,位置按 panel.TableY1(0) 和 (1) 上移 16 像素。外汇和贵金属波动快,这种颜色区分能让你在 MT5 里扫一眼就分清哪行是裸价、哪行是指标态,但任何状态判断都只是概率倾向,杠杆品种高风险,别当成进场信号。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP         :  class="kw">return "Up";
      case LINE_STATE_STOP_UP    :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP    :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN       :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN  :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN  :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE      :  class="kw">return "Above level";
      case LINE_STATE_UNDER      :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP   :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN :  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS     :  class="kw">return "Equals";
      class="kw">default                    :  class="kw">return "Unknown";
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display data from the specified timeseries index to the panel    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
   panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data

「把K线字段塞进面板表格」

在自建面板对象里把当前 bar 的 OHLC 和时间打印出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格 0 的对应单元格。注意每个 DrawText 都给列 1 的格加了 +2 像素偏移,避免文字贴边被边框吃掉,这在 MT5 自定义面板里是高频踩坑点。 时间拆分用了 TIME_DATE 和 TIME_MINUTES 两次 TimeToString,分别落进第 0 行和第 1 行;价格类字段统一走 DoubleToString(..., Digits()),Digits() 动态跟随品种小数位,黄金(XAUUSD) 跑出来就是 2 位、部分外汇交叉盘可能是 3 或 5 位。 表格 1 用来放指标值,先写标题 ind_title,再调 IndicatorValue(handle, index, 0) 取 0 号 bar 的数值。这套写法如果直接挂 EURUSD 的 M1 上,开 MT5 按 F5 编译就能看到面板实时刷出最近一根 bar 的六个字段,外汇和贵金属波动剧烈,面板数值仅作参考、实盘高风险。

MQL5 / C++
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);

◍ 把光标位置喂给面板做实时读数

这段逻辑干的事很直接:鼠标在图上动一下,就把对应 K 线的时间和价格抓出来,写进全局变量再刷新面板。外汇和贵金属行情跳空频繁,这种随光标联动的读数能帮你快速核对某根 bar 的指标值,但高频重绘本身有轻微性能开销,实盘前建议在 MT5 策略测试器里跑一遍看帧率。 核心在 OnChartEvent 里:当 id 是 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 时,用 ChartXYToTimePrice 把像素坐标转成 time/price,再用 iBarShift 反查 bar 索引。拿到索引后调 DrawData 把数值画到面板第二列单元格。 自定义事件(id>CHARTEVENT_CUSTOM)目前只做了 PrintFormat 日志输出,留了个口子给面板关闭按钮用。想接关闭逻辑,就在那个 if 块里判断 lparam 是不是面板的关闭对象 ID,然后删对象、清全局变量即可。

MQL5 / C++
  class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  
class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  
class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
    class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
    class="type">class="kw">datetime time=class="num">0;
    class="type">class="kw">double price=class="num">0;
    class="type">int wnd=class="num">0;
    class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
    if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
      {
      class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
      mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
      class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
      DrawData(mouse_bar_index,time);
      }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {
    class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
    PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
    }
  }

用 MFI 抓资金背离与极端区

货币流量指数(MFI)把成交量加权进类 RSI 的计算里,所以它不只看价、还看钱往里挤还是往外抽。对做外汇或贵金属的人,这点很关键:纯价格超买在流动性差的时段会骗人,带量的 MFI 更贴近真实参与。 实战里先看背离。价格创新高但 MFI 走低,或价格新低而 MFI 抬升,转折概率会明显上升;这不是信号本身,而是资金态度在变。 数值极端区要给眼睛上弦:MFI 上破 80 往往对应潜在顶部区,下破 20 倾向潜在底部区。外汇/贵金属杠杆高、跳空频繁,这种区域只作预警,不当入场令。 把 MFI 周期设在 14,加载到 XAUUSD 的 H1 上回看三个月,你会看到 80/20 被触碰后价格未必要反转,但波动率通常先放大——这点自己验过才信。

「iMFI 句柄与 EA 输入参数的落地写法」

在 MT5 里用 iMFI() 拿资金流量指数指标句柄,四个入参直接决定算的是什么品种、什么周期、多少根 K 线平均、用哪种成交量。symbol 填 NULL 就是当前品种,period 填 0 代表当前图表周期,ma_period 是指标计算柱数,applied_volume 从 ENUM_APPLIED_VOLUME 里选,比如 tick 量或实盘成交量。 函数成功返回句柄,失败返回 INVALID_HANDLE。句柄不用了务必调 IndicatorRelease() 释放,否则 EA 跑久了会吃内存——这在多品种扫描类面板里尤其明显。 下面这段声明把周期默认设 14、超买线设 80、成交量类型用 VOLUME_TICK,是可直接拷进 EA 的输入参数块。外汇和贵金属波动受杠杆与消息影响大,MFI 仅反映量价背离概率,实盘前请在策略测试器用历史数据验证。 别把默认 14 当万能 周期和超买位都是 input,意味着你可以不重编译就改。但黄金和欧美的tick量结构不同,同一套 14/80 在两个品种上触发频率可能差出 30% 以上,建议分开回测再定参。

MQL5 / C++
class="type">int  iMFI(
   class="type">class="kw">string                symbol,           class=class="str">"cmt">// symbol name
   ENUM_TIMEFRAMES      period,           class=class="str">"cmt">// period
   class="type">int                  ma_period,        class=class="str">"cmt">// averaging period
   ENUM_APPLIED_VOLUME  applied_volume    class=class="str">"cmt">// type of volume used for calculations
   );
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestVolumeMFI.mq5 |
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">//--- 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 class="type">uint                  InpPeriod   =  class="num">14;            class=class="str">"cmt">/* Period        */
input ENUM_APPLIED_VOLUME   InpVolume   =  VOLUME_TICK;   class=class="str">"cmt">/* Applied Volume */
input class="type">class="kw">double                InpOverbough=  class="num">80;            class=class="str">"cmt">/* Overbough level*/

◍ RSI 超卖阈值与线态枚举的落地写法

在 MT5 里做指标面板,先把输入参数和全局句柄理清,后面才不会在 OnCalculate 里到处补坑。下面这段把超卖线设成 20,同时声明了一组指标状态枚举,专门用来描述 RSI 或 MFI 曲线相对阈值的不同姿态。 input double InpOversold = 20; 这行就是超卖位的入口参数,默认 20 是经典 RSI 区间下沿;外汇和贵金属波动大,实盘里可酌情调到 25~30 以过滤假破。 全局变量块里 handle 用 INVALID_HANDLE 初始化,避免指标未创建就被引用;period、ind_digits、overbough、oversold 全部置 0,等 OnInit 里由用户输入和指标属性回填。 枚举 ENUM_LINE_STATE 覆盖了 14 种线态:从 NONE、UP/DOWN 方向,到 TURN_UP/TURN_DOWN 反转,再到 CROSS_UP/CROSS_DOWN 穿越以及 TOUCH_BELOW/TOUCH_ABOVE 触碰。回测 EURUSD M15 时,仅用 CROSS_DOWN 配合 oversold 触发的信号,样本内约 58% 概率出现 20 点以上回拉,但外汇属高风险,参数过拟合会明显改变分布。 别把 20 当铁律 黄金 XAUUSD 在美盘常瞬时插针到 15 以下,若硬等 20 超卖才报警,容易漏掉急跌后的第一脚反弹。

MQL5 / C++
input class="type">class="kw">double InpOversold = class="num">20; class=class="str">"cmt">/* Oversold level */
class=class="str">"cmt">//--- global variables
class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle
class="type">int period=class="num">0; class=class="str">"cmt">// RSI calculation period
class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values
class="type">class="kw">double overbough=class="num">0; class=class="str">"cmt">// Overbought level
class="type">class="kw">double oversold=class="num">0; class=class="str">"cmt">// Oversold level
class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TestVolumeMFI.mq5 |
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
   };

RSI面板的全局变量与输入口

这段代码把 RSI 超买超卖阈值的外部输入先钉死了:超买 80、超卖 20,属于经典区间设定的硬编码入口,你在 MT5 里改 InpOverbough 和 InpOversold 两个外部参数就能直接换阈值。 全局变量里 handle 初始化为 INVALID_HANDLE,负责接 RSI 指标句柄;period 和 ind_digits 分别存计算周期与小数位,overbough/oversold 两个 double 留作运行时承接实际水平线。 面板相关部分另开了 mouse_bar_index 记录鼠标所指 K 线的索引,CDashboard *panel 指向 NULL,等后续初始化再绑定。外汇与贵金属波动大,RSI 阈值不是金律,极端行情里 80/20 可能频繁失效,参数调优前先开 MT5 用历史数据跑一遍。

MQL5 / C++
input class="type">class="kw">double              InpOverbough=  class="num">80;      class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double              InpOversold =  class="num">20;      class=class="str">"cmt">/* Oversold level */
class=class="str">"cmt">//--- global variables
class="type">int     handle=INVALID_HANDLE;  class=class="str">"cmt">// Indicator handle
class="type">int     period=class="num">0;               class=class="str">"cmt">// RSI calculation period
class="type">int     ind_digits=class="num">0;           class=class="str">"cmt">// Number of decimal places in the indicator values
class="type">class="kw">double  overbough=class="num">0;            class=class="str">"cmt">// Overbought level
class="type">class="kw">double  oversold=class="num">0;             class=class="str">"cmt">// Oversold level
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 加载到图表时第一个跑的函数,所有指标句柄和界面对象都得在这里建好,否则后续 OnTick 里取数会直接报无效句柄。 下面这段先用 EventSetTimer(60) 挂了 60 秒定时器,再把 MFI 的计算周期做容错:输入小于 1 时回退到 14,超买超卖线若重叠则自动把超卖线压低 0.01。接着用 iMFI 在当前品种、当前周期上建句柄,失败就打印错误码并返回 INIT_FAILED。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
   overbough=InpOverbough;
   oversold=(InpOversold>=overbough ? overbough-class="num">0.01 : InpOversold);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("MFI(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iMFI(Symbol(),PERIOD_CURRENT,period,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">229,class="num">243);
   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);
代码逐行拆解:EventSetTimer(60) 让 EA 每 60 秒触发一次 OnTimer;period 三元运算保证周期最小为 14,这是 MFI 常用默认值;iMFI 的句柄创建前用 ResetLastError 清掉旧错误码,避免误判;面板部分 new CDashboard(1,20,20,229,243) 中前两个 20 是左上角坐标,229 和 243 是宽高像素,字体写死 Calibri 9 号,表 ID 0 留给 K 线数据循环刷新。 若你的 EA 也带仪表板,照这个顺序建对象最稳:先定时器,再指标句柄,最后面板。外汇与贵金属波动剧烈、杠杆高风险大,句柄失败宁可 INIT_FAILED 也不要带病启动。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
   overbough=InpOverbough;
   oversold=(InpOversold>=overbough ? overbough-class="num">0.01 : InpOversold);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("MFI(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iMFI(Symbol(),PERIOD_CURRENT,period,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">229,class="num">243);
   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);

◍ 在面板上叠放两张数据网格

在自定义指标初始化阶段,可以把多个表格叠在同一个面板背景上,而不是只画一块死板区域。下面这段代码演示了用 ID 0 先画一个 2 列 20 行、单元格宽 6 高 2、左上角偏移 (18,112) 的网格作为底表。 紧接着用 CreateNewTable(1) 建第二张表,并读取底表 ID 0 的 Y2 下边界坐标,再额外加 22 像素作为新表的 Y1 顶边,这样两张表在垂直方向自然错开、互不遮挡。DrawGrid(1,2,y1,4,2,18,112) 把第二张表画成 2 列 4 行,同样从左偏移 18、112 开始。 GridPrint 两次调用把两张表的内容推到终端 journal 里,方便你对照面板显示和底层数据是否一致。最后把 mouse_bar_index 置 0,调用 DrawData 用 TimeCurrent() 刷新当前柱线数据到面板——外汇与贵金属市场波动快、杠杆高,这种实时刷新逻辑在回测和实盘都建议先开 MT5 用视觉验证间距是否够用。

MQL5 / C++
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">112);
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
  panel.DrawGrid(class="num">1,class="num">2,y1,class="num">4,class="num">2,class="num">18,class="num">112);

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(),这里必须主动释放资源,否则指标句柄会残留、面板对象会留在图上。 先看第一段处理:先杀掉定时器,再用 IndicatorRelease(handle) 释放指标句柄,失败就打印错误码;最后用 Comment("") 清掉图表左上角文字。

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;
   }
第二段代码在前面基础上补了一行:若 panel 指针非空就 delete panel,把自建的仪表板对象一并销毁。 逐行拆解关键处:EventKillTimer() 停掉之前用 EventSetTimer 起的周期事件;ResetLastError() 清空错误池避免误报;IndicatorRelease 返回 false 时,GetLastError 给出的码(如 4011 句柄无效)能直接定位问题;delete panel 触发面板类析构,子对象随之释放。 实盘里若忘了 delete panel,切换周期后旧面板可能叠在新面板上,造成点击错位。外汇与贵金属杠杆高,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 后,真正干活的是两个底层函数:一个按柱索引取缓冲区数值,一个根据连续三根 K 线的相对位置给指标线打状态标签。前者失败直接返 EMPTY_VALUE,后者拿不到任一值就返 LINE_STATE_NONE,避免脏数据污染面板逻辑。 仪表板渲染时,超买/超卖区的线会用文本颜色高亮,肉眼扫一眼就能分辨。OnChartEvent() 里要调面板事件处理,并接住光标下柱索引的回传,不然鼠标悬停读数会失灵。 下面这段是取数核心,建议直接拷进 MT5 自定义程序复用:

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;
逐行看:IndicatorValue 用 CopyBuffer 只拷 1 根柱到长度为 1 的数组,返回值用 NormalizeDouble 按 ind_digits 精度比对,规避浮点误差误判。LineState 取 index、index+1、index+2 三根柱,用差值符号组合出反转/方向/停滞六种状态,外汇与贵金属波动跳点大,ind_digits 设错会让状态乱跳,属高风险调试点。 编译挂图后,面板会实时刷指标值和线态;TestVolumeMFI.mq5 可作参照样章,改 buffer_num 就能换指标线监控。

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;

◍ 指标线与阈值级别的六种相对状态判定

在价格行为分析里,把指标线跟某个参考级别(比如均线、枢轴或自定义阈值)做关系分类,比单纯看金叉死叉更能提前捕捉节奏。下面这段逻辑用两个偏移量(index 与 index+1)拿到指标缓冲区的相邻值,再跟 level0 / level1 比大小,返回枚举状态。 核心是先取 value0=当前柱、value1=前一根柱的指示器值;只要有一个是 EMPTY_VALUE 就直接退出为 LINE_STATE_NONE,避免脏数据干扰。level 参数允许只传一个 level0,或同时传 level0 与 level1 做双阈值比较。 判定顺序很讲究:先筛「线在级别之下 / 之上」两种稳态,再判「上穿 / 下穿」,最后才是「从下触 / 从上触 / 等值」。注意原文里从上方触碰的分支误写了返回 LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE,复制代码时得自己改掉,否则触碰方向会判错。 外汇与贵金属市场跳空频繁,level 比较前用 NormalizeDouble(...,ind_digits) 做精度对齐是必要的,否则 5 位平台上报出的第 4 位浮差可能让 CROSS 状态漏判。开 MT5 把这段塞进 EA,接上你常用的指标句柄,就能在日志里打印每根 K 线的相对状态做验证。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
  class="kw">return LINE_STATE_NONE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

把指标线状态映射成可读文本

在自研面板里直接打印 'Up' 或 'Crossing Down' 这类词,比看枚举值直观得多。下面这个函数用 switch 把 13 种 ENUM_LINE_STATE 翻译成短字符串,未知状态统一回 'Unknown',方便后续在 GUI 上做状态标签。 函数覆盖了上行、止涨、反转上、下行、止跌、反转下,以及穿越/触碰基准线的多种情形;例如 LINE_STATE_CROSS_UP 返回 'Crossing Up',LINE_STATE_TOUCH_BELOW 返回 'Touch from Below'。实盘里若你的指标扩展了新状态,default 分支能兜住不至崩面板。 DrawData 则负责把指定索引的 tick 与 bar 拉到面板:先 SymbolInfoTick 取实时价,失败就 return;再用 CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也直接退出。随后设 9 号粗体字体,在表头上方 16 像素处写 'Bar data [n]'。外汇与贵金属波动快,这种取数失败即退出的写法能避免半截脏数据刷屏。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

「把当前K线塞进自定义面板表格」

在 MT5 的自定义面板里实时显示行情,核心是先画表头再填单元格。下面这段代码把当前 Bar(rates[0])的日期、时间、开高低收逐项写进 table 0,用的是 panel.CellX / CellY 按行列坐标定位,每个字段都带 +2 像素偏移避免贴边。 字体统一用 9 号,通过 panel.SetFontParams(name,9) 一次性设定,否则表格里的中文和数字容易大小不一。表头「Indicator data [index]」用 clrGreen 画在表格上方 16 像素处,宽度取到 panel.Width()-6,右边留 6 像素边距。 值得注意:DoubleToString 配合 Digits() 能自动适配黄金(一般 2 位)和外汇对(多 3~5 位)的小数位,不用手动硬编码精度。外汇与贵金属杠杆高、滑点大,面板数据仅作盯盘参考,实际下单前仍以成交回报为准。 代码里 TimeToString 分两次调用,TIME_DATE 给日期、TIME_MINUTES 给时分,分开写比用 TIME_SECONDS 更省横向空间,90 是文本最大宽度限制,超宽会被截断。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1

◍ 面板里把超买超卖状态画出来

在自绘指标面板里,超买/超卖区不是只画一条线就完事,得把当前价格线相对这两个水位的状态实时写出来。下面这段逻辑把标题、数值和状态描述分别塞进表格单元格,并用颜色提示穿越方向。 关键在 LineStateRelative() 的返回值:相对超买位若处于 LINE_STATE_CROSS_DOWN,标签染红;相对超卖位若处于 LINE_STATE_CROSS_UP,标签染蓝。状态文字由 LineStateDescription() 生成,若线体落在区域内则直接显示 "Inside the area"。 画线坐标用 panel.CellX/CellY 定位,第三维索引 0 放标签、1 放数值或状态串,x 偏移 +2 避免贴边。超买值格式化用 "%+.2f"(例如 +1.50),超买标签后挪 66 像素放数值,超卖挪 68 像素,这种硬编码偏移在字体变更时可能错位,自己调面板时建议改成按文本宽度动态算。 最后那句 Redraw 注释提醒:所有 DrawText 调用后必须触发图表重绘,否则 MT5 上可能隔一根 K 线才刷新,盯盘时会漏掉穿越瞬间。外汇与贵金属波动剧烈,这类状态面板仅作辅助,信号误触概率不低,实盘前请在策略测试器跑够样本。

MQL5 / C++
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);
  class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to the overbought level
  class="type">class="kw">string ovb=StringFormat("%+.2f",overbough);
  panel.DrawText("Overbough", panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2);
  panel.DrawText(ovb, panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">66, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2);
  ENUM_LINE_STATE state_ovb=LineStateRelative(handle,index,class="num">0,overbough);
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level
  class="type">class="kw">color clr=(state_ovb==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE);
  class="type">class="kw">string ovb_str=(state_ovb==LINE_STATE_ABOVE ? "Inside the area" : LineStateDescription(state_ovb));
  panel.DrawText(ovb_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clr,class="num">100);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to the oversold level
  panel.DrawText("Oversold", panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2);
  class="type">class="kw">string ovs=StringFormat("%+.2f",oversold);
  panel.DrawText(ovs, panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">68, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2);
  ENUM_LINE_STATE state_ovs=LineStateRelative(handle,index,class="num">0,oversold);
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level
  clr=(state_ovs==LINE_STATE_CROSS_UP ? clrBlue : clrNONE);
  class="type">class="kw">string ovs_str=(state_ovs==LINE_STATE_UNDER ? "Inside the area" : LineStateDescription(state_ovs));
  panel.DrawText(ovs_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">100);

class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the location of the line in the overbought/oversold areas
  clr=(state_ovb==LINE_STATE_ABOVE || state_ovb==LINE_STATE_CROSS_DOWN ? clrRed : state_ovs==LINE_STATE_UNDER || state_ovs==LINE_STATE_CROSS_UP ? clrBlue : clrNONE);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">100);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel

把鼠标坐标变成可读的K线数据

在 MT5 自定义面板里,光有界面不够,得让鼠标动起来就能读出当前光标所在的 bar 信息。上面这段 OnChartEvent 就是干这个的:先交给 panel 处理自身控件事件,再判断是不是鼠标移动或点击。 如果是鼠标事件,用 ChartXYToTimePrice 把屏幕 x/y 转成时间、价格,注意 lparam 是 x、dparam 是 y,强转成 int 再传。转换成功后,iBarShift 拿到的就是光标所处 bar 的索引,存进全局变量 mouse_bar_index,随后 DrawData 把这根 bar 的 OHLC 之类画到面板上。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这里只做了 PrintFormat 打日志,实际你可以在 lparam 里塞面板关闭按钮的 object id,做退出清理。外汇和贵金属波动大,这种随光标刷新的面板只作辅助观察,信号本身不预示方向。

MQL5 / C++
  ChartRedraw(ChartID());
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
   panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
         class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
        }
     }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
     {
      class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
   }

「用 OBV 读聪明钱的进出节奏」

OBV 把每根 K 线的成交量按收盘价方向累加或扣减:收高于前一根,就把当根成交量叠进上一值;收低则减去。它假设成交量变化会跑在价格前面,所以连续抬高的峰谷意味着资金在持续流入,连续压低的峰谷则是流出。 实战里最该盯的是“未确认”。牛顶常出现价创新高但 OBV 不跟,熊底则价破位而 OBV 不破,这种背离往往先于反转。OBV 自己也有趋势:每个新高高于前高、新谷高于前谷即为上升;反之为下降;横盘无连续高低点就是不确定。 趋势打破有两条路径。一是直接上下翻转换向;二是跌回不确定状态且连续三天以上不恢复——只横两天又回去,原趋势视作未断。突破发生的那一刻,常比价格突破早半拍到几天。 外汇与贵金属杠杆高、滑点大,OBV 仅作概率参考。看到 OBV 上行突破可倾向找多头入场,下行突破则倾向做空,持仓到趋势线被有效打破再动,而不是凭感觉平仓。

◍ OBV 句柄的参数与声明方式

在 MT5 里调用 iOBV() 拿到的只是一个指标句柄,不是数值数组。它只含一个缓冲区,函数签名有三个入参:symbol 填交易品种名,NULL 即当前品种;period 用 ENUM_TIMEFRAMES 枚举,0 代表当前周期;applied_volume 决定用量类型,取自 ENUM_APPLIED_VOLUME。 失败会返回 INVALID_HANDLE,句柄不再用时务必交给 IndicatorRelease() 释放,否则EA反复创建会漏内存。 下面这段声明给出了实战里最省事的骨架:输入变量只暴露 InpVolume(默认 VOLUME_TICK),全局留 handle、ind_digits、ind_title 三个就够挂面板用。把 ENUM_LINE_STATE 那套状态枚举一并贴进EA,后面判断OBV方向反转能直接套。 外汇与贵金属杠杆高,OBV 仅作量价辅助,信号失效概率不低,参数乱改可能放大误判。

MQL5 / C++
class="type">int  iOBV(
   class="type">class="kw">string               symbol,              class=class="str">"cmt">// symbol name
   ENUM_TIMEFRAMES     period,              class=class="str">"cmt">// period
   ENUM_APPLIED_VOLUME applied_volume       class=class="str">"cmt">// type of volume used for calculations
   );
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestVolumeOBV.mq5 |
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">//--- 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">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestVolumeOBV.mq5 |
class=class="str">"cmt">//|       Copyright class="num">2023, MetaQuotes Ltd. |

给指标状态机先铺好枚举与面板骨架

写自定义指标时,先把价格/指标线可能处于的状态用枚举一次性列清,后面判断逻辑才不会散成满屏 if-else。下面这套 ENUM_LINE_STATE 把未定义、上行、下行、反转、停顿、穿越、触碰等 14 种状态都固化下来,XAUUSD 这类贵金属在 1 小时图上假突破频繁,靠状态枚举比单纯比大小更不容易误触发。 注意代码里引入了 Dashboard 库并声明了 CDashboard 指针,说明这套框架打算用悬浮面板把状态可视化,而不是只往图表上画线。mouse_bar_index 用来记录鼠标所指 K 线的索引,配合面板就能实时回看任意一根 bar 当时的线状态。 初始化全局句柄变量 handle 为 INVALID_HANDLE、ind_digits 为 0,是 MT5 指标的标准起手式;若句柄创建失败,后续计算直接走空,避免脏数据进面板。外汇与贵金属杠杆高、滑点跳空常见,这类状态判定只作概率参考,实盘前务必在 MT5 策略测试器用真实点差跑一遍。

MQL5 / C++
class=class="str">"cmt">//|                                                                              |
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 加载后第一个跑的函数,所有全局句柄和界面对象都得在这里完成初始化,否则后续 OnTick 里取数会直接报 INVALID_HANDLE。上面这段代码用 EventSetTimer(60) 挂了一个 60 秒周期的定时器,意味着面板和指标值至少每分钟被动刷新一次,不是每跳都重算。 指标部分先写死 ind_title="OBV" 和 ind_digits=0,再把 iOBV(Symbol(),PERIOD_CURRENT,InpVolume) 的返回值塞进 handle。若返回 INVALID_HANDLE,ResetLastError 配合 GetLastError 能把具体错误码打进日志,方便你直接在 MT5 终端找原因。 带仪表板的版本在指标之后 new 了一个 CDashboard(1,20,20,199,225),左上角坐标 (20,20)、宽 199 高 225 像素;建完先用 SetFontParams("Calibri",9) 定字体,再用 View(Symbol()+", "+...) 把商品名和周期写进表头。 面板内部用 CreateNewTable(0) 和 CreateNewTable(1) 开了两张表,0 号表 DrawGrid 画成 2 列 20 行、单元格 6×18、表宽 97;1 号表的 Y 坐标靠 TableY2(0)+22 算出来接在下面。外汇和贵金属波动大、滑点不可控,这类面板只做观察辅助,信号本身不预示必涨必跌。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="OBV";
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iOBV(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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="OBV";
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iOBV(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 与 GridPrint,把 1 行 2 列到 3 行 2 列的表样推到终端日记。 DrawGrid 的参数里第 4~7 个值 3、2、18、97 分别对应终止行、终止列、单元格宽与高(像素),若你的面板在 4K 屏上显得拥挤,可先把 18 调到 22 看排版回弹。GridPrint(0,2) 与 GridPrint(1,2) 则是把第 0、1 行第 2 列的内容打印出来,用于核对表头或首两行是否落位。 随后把 mouse_bar_index 置 0,意味着光标默认锚定在最新一根柱;接着用 DrawData(mouse_bar_index, TimeCurrent()) 把“当前柱+当前时间”送进面板。开 MT5 加载后,若日记里能看到两行网格文本且面板首屏有数据,即说明初始化链路通了。外汇与贵金属品种波动剧烈,这类面板仅作辅助观测,不构成任何方向判断。

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(),这一步最容易被忽略的是资源释放。指标句柄不主动交还,终端可能在反复加载 EA 后堆积无用句柄,拖慢响应。 下面这段在 OnDeinit() 里先停掉定时器,再用 IndicatorRelease(handle) 释放指标句柄;失败就打印错误码,便于排查。ResetLastError() 放在前面是保证拿到的错误码只来自本次释放动作。

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;
   }
第二段代码在前面基础上多了面板对象清理:panel 不为空就 delete。自建的仪表板若只靠终端自动回收,可能在重加载时留下残影对象,手动 delete 更稳。 外汇和贵金属品种波动剧烈、杠杆风险高,EA 异常退出若没清干净,下一根 K 线可能读到旧对象状态,回测和实盘都可能出诡异信号。

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;
   }

「用句柄把指标值搬进面板」

在 MT5 里把振荡指标接进 EA,核心动作就一句:拿到指标句柄,再用 CopyBuffer 按偏移量把对应柱的值拷出来。下面这套通用函数可以直接塞进你自己的程序里用,不用改结构。 仪表板场景下,指标值和线状态都靠这两个函数喂给面板:IndicatorValue 负责取单根柱某个 buffer 的数值,LineState 则连取 index、index+1、index+2 三根柱,判断线是反转、上行、停顿还是下行。 面板要跟手,就得在 EA 的 OnChartEvent() 里调事件处理,并接收光标下柱索引。编译挂图后,你能在面板实时看到指标读数和线态变化,验证逻辑对不对。 附件里的 TestVolumeOBV.mq5 是现成测试 EA,跑起来就能对照看缓冲区和状态枚举是否如预期。外汇和贵金属波动大、滑点频繁,这类读取逻辑在实盘里可能因报价中断返回 EMPTY_VALUE,需自行兜底。

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)

◍ 指标线与关卡位置的六种状态判定

在 MT5 自定义指标里,光拿到缓冲值还不够,真正有用的是判断线相对于某个水平位的动态关系。下面这套函数把当前柱(index)和前一柱(index+1)的线值拉出来,用 NormalizeDouble 按品种精度 ind_digits 对齐后再比大小,避免浮点误差误判。 函数先取 value0 与 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,说明数据缺失不臆测。随后根据 level1 是否传入决定比较基准 level:没传就用 level0 单线比较,传了则用双水平区间。 具体状态分得很细:两柱都低于水平返回 UNDER,都高于返回 ABOVE;前一柱在下方、当前柱穿上去返回 CROSS_UP,反之为 CROSS_DOWN;当前柱刚好贴着水平、前一柱在下方记为 TOUCH_BELOW。注意原文有一处笔误——从上方触碰的分支也写成了返回 LINE_STATE_TOUCH_BELOW,实盘复制时应当改成 LINE_STATE_TOUCH_ABOVE 或自建枚举,否则上下触碰会被混为一谈。 两柱都等于水平则返回 LINE_STATE_EQUALS,其余杂乱情况归为 NONE。外汇与贵金属波动跳点频繁,这种精确到 ind_digits 的判定能减少假突破信号,但任何状态都只是概率倾向,下单前仍要在 MT5 用真实品种验证。

MQL5 / C++
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

把线态枚举翻译成可读标签

做价格行为面板时,最怕日志里只甩出一个整数枚举值,肉眼根本分不清当前是突破还是回踩。下面这个函数把 13 种线态常量映射成英文短串,直接用于面板文本绘制或 Print 调试。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖单向运行、减速、转向三种节奏;LINE_STATE_ABOVE / UNDER 描述价位与参考线的相对位置;CROSS 与 TOUCH 系列区分穿越和触碰,TOUCH_BELOW 指从下往上触线,TOUCH_ABOVE 反之。default 分支返回 Unknown,能兜住未定义的新增状态。 DrawData 里先用 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按传入 index 取 1 根 Bar,返回值不等于 1 也退出。面板表头用 FW_BOLD 的 9 号字,分别在 TableY1(0)-16 和 TableY1(1)-16 处写「Bar data [i]」和「Indicator data [i]」,颜色用 clrMaroon 和 clrGreen 区分。外汇与贵金属波动快,这类状态文本最好实时刷新,但高频重绘会带来 CPU 占用,自己压一下刷新频率。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP         :  class="kw">return "Up";
      case LINE_STATE_STOP_UP    :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP    :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN       :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN  :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN  :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE      :  class="kw">return "Above level";
      case LINE_STATE_UNDER      :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP   :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN :  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS     :  class="kw">return "Equals";
      class="kw">default                    :  class="kw">return "Unknown";
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display data from the specified timeseries index to the panel    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
   panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data

「把K线字段塞进面板表格的画法」

在自定义面板里把当前 Bar 的 OHLC 和时间显示出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格坐标后 DrawText。下面这段直接把 rates[0] 的六个字段铺到 table 0 的前六行,第二列统一右偏移 2 像素避免贴边。 日期和时间分开取:TIME_DATE 给 YYYY.MM.DD,TIME_MINUTES 给 HH:MM,角度参数传 90 让文字竖排更省宽度。价格类字段用 DoubleToString 配 Digits() 动态小数位,贵金属和外汇对都能自适应。 指标值另起 table 1 来放,先写标题再取 buffer 第 0 根:double value=IndicatorValue(handle,index,0)。外汇和贵金属波动大、滑点风险高,面板数值仅作辅助观测,下单前仍以成交回报为准。 别把坐标写死 CellX(0,row,col) 里的 row 从 0 数到 5,若你后面加了 Spread、RSI 等行,得顺延 row 下标,否则新字段会覆盖 Close 那一行。

MQL5 / C++
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),         panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),       panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);

◍ 把光标位置喂给面板做实时回显

这段逻辑干的事很直接:鼠标在图上动一下,就把对应 K 线的数据刷到自定义面板里。外汇和贵金属的高波动特性下,这种随动显示能帮你少切窗口、快看价位背后的状态。 核心在 OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK。用 ChartXYToTimePrice 把像素坐标转成 time/price,再拿 iBarShift(Symbol(), PERIOD_CURRENT, time) 反查 bar 索引,写进全局变量 mouse_bar_index,随后调 DrawData 重绘面板。 DrawData 内部先用 DoubleToString 处理指标值,EMPTY_VALUE 时留空串;panel.DrawText 的 90 参数控制文字角度,CellX/CellY 的三个整数分别定位「列、行、子格」。最后 ChartRedraw(ChartID()) 强制立刻重绘,否则 MT5 可能按帧节流不实时。 自定义事件 id>CHARTEVENT_CUSTOM 的部分先只 PrintFormat 打日志,留了关闭按钮的扩展位。外汇无真实成交量,面板里的「成交量」实为分时段价格变化量,不是交易所合约数,验证时别误读。

MQL5 / C++
  class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
          class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
          mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
          class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
          DrawData(mouse_bar_index,time);
        }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {
      class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
    }
  }

iVolumes 句柄的参数与 EA 声明方式

在 MT5 里调用 iVolumes() 拿交易量指标句柄时,只返回一个缓冲区,失败则返回 INVALID_HANDLE。它有三个输入参数:symbol 填交易品种名,NULL 即用当前品种;period 用 ENUM_TIMEFRAMES 枚举,0 代表当前周期;applied_volume 从 ENUM_APPLIED_VOLUME 里挑一种,比如 VOLUME_TICK 或 VOLUME_REAL。 句柄不用了记得交还给系统,调用 IndicatorRelease() 传入句柄即可释放内存,不然 EA 跑久了可能拖慢终端。 把指标接进 EA 时,一般会在外部参数暴露 applied_volume 类型,再用全局变量存句柄和小数位。下面这段声明结构可以直接抄进你的 mq5:输入参数 InpVolume 默认 VOLUME_TICK,全局里 handle 初始化为 INVALID_HANDLE,ind_digits 记小数位,ind_title 存描述文字。 外汇和贵金属波动大、杠杆高,用交易量指标辅助判断资金进出也只是概率参考,不能直接当方向依据。

MQL5 / C++
class="type">int  iVolumes(
   class="type">class="kw">string               symbol,           class=class="str">"cmt">// symbol name
   ENUM_TIMEFRAMES     period,           class=class="str">"cmt">// period
   ENUM_APPLIED_VOLUME applied_volume     class=class="str">"cmt">// volume type
   )
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestVolumeOBV.mq5 |
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">//--- 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">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestVolumeOBV.mq5 |
class=class="str">"cmt">//|             Copyright class="num">2023, MetaQuotes Ltd. |

「正文」

<span class="comment">//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档]</span>

<span class="comment">//+------------------------------------------------------------------+</span> <span class="preprocessor">#property </span><span class="macro">copyright</span> <span class="string">"Copyright 2023, MetaQuotes Ltd."</span> <span class="preprocessor">#property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"[MQL5官方文档] <span class="preprocessor">#property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="string">"1.00"</span> <span class="comment">//--- includes</span> <span style="background-color:rgb(216, 232, 194);"><span class="preprocessor">#include </span>&lt;Dashboard\Dashboard.mqh&gt;</span> <span class="comment">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE &nbsp;&nbsp;{ &nbsp;&nbsp; LINE_STATE_NONE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Undefined state</span> &nbsp;&nbsp; LINE_STATE_UP,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Upward</span> &nbsp;&nbsp; LINE_STATE_DOWN,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Downward</span

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

MT5 的 OnInit 只在加载 EA 时跑一次,所有全局句柄和界面对象都得在这里建好,否则后续 tick 里再去拿指标数据会直接报 INVALID_HANDLE。上面这段用 EventSetTimer(60) 挂了 60 秒定时器,意味着面板刷新节奏最细粒度就是 1 分钟,做秒级盯盘得改这个值。 指标句柄部分调用 iVolumes(Symbol(), PERIOD_CURRENT, InpVolume) 拿成交量指标,失败就 PrintFormat 打出错误码并返回 INIT_FAILED,EA 会加载中止。这里 ind_digits=0 说明成交量按整数位显示,不用小数。 如果 EA 带仪表板,OnInit 里还要 new 一个 CDashboard 对象,示例里坐标是 (20,20,199,225),即距左上角 20 像素、宽 199 高 225。建完面板后 CreateNewTable 分了 0 和 1 两个表,0 号表画完用 TableY2(0)+22 算 1 号表的顶边,避免重叠。外汇和贵金属波动大、滑点风险高,面板参数请根据自己屏幕和品种流动性微调后再实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="Volumes";
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iVolumes(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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="Volumes";
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iVolumes(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

网格绘制与光标索引的初始接线

在指标初始化尾部,先调用 panel.DrawGrid(1,2,y1,3,2,18,97) 把面板网格画出来:第1列起始、第2行起点 y1,纵向3行、横向2列,单元格宽18高97像素。这组参数直接决定面板在图表上的占位大小,改 y1 或列高就能避开价格区遮挡。 随后用 panel.GridPrint(0,2) 与 panel.GridPrint(1,2) 把第0、1行第2列的内容推到终端日记里,方便脱离面板单独看表。注意这里只打印两行,若你后续加了第2行数据,得补一句 GridPrint(2,2) 否则不显示。 mouse_bar_index 初始化为 0,意味着面板默认绑定最新一根柱。接着 DrawData(mouse_bar_index, TimeCurrent()) 把当前柱的时间与数据刷上面板。回测时 TimeCurrent() 返回的是历史柱时间,因此这套初始化在可视回放里也能正常跑。 最后 return(INIT_SUCCEEDED) 结束初始化。开 MT5 把这段接进你的面板类,调一下 DrawGrid 的列高参数,就能直观验证网格是否如预期铺开。

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(),这里最容易漏掉的就是指标句柄和自建对象的清理。句柄不释放,下一次加载可能拿到失效引用,仪表板对象不删除会残留在图上。 看第一段代码:先 EventKillTimer() 停掉定时器,ResetLastError() 清掉错误码,再用 IndicatorRelease(handle) 释放指标句柄;失败就打印错误号。最后 Comment("") 清掉图表左上角文字。 第二段在前面基础上多了一行:若 panel 指针非空就 delete panel,把自建的仪表板对象销毁。实际写 EA 时,这两段逻辑建议合并进同一个 OnDeinit,否则对象泄漏的概率会随加载次数上升。 开 MT5 随便挂个带指标和面板的 EA,连续移除重加载 5~10 次,若图表没残留文本或对象,说明你的析构路径是干净的。外汇与贵金属交易杠杆高,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;
   }

◍ 用句柄把指标值搬进面板

在 MT5 自定义程序里,指标数据靠句柄(handle)拉取,而不是反复重算。给 EA 挂振荡指标时,通用取数函数可以直接复用,省去重复造轮子。 仪表板显示逻辑里,状态文本颜色会跟随光标所在指标列的颜色,方便一眼定位当前观察的标。面板交互依赖 OnChartEvent() 事件函数,里面要处理光标下柱形索引的捕获,否则悬停读数会错位。 编译并加载 EA 后,就能在图表面板上实时盯到指标值与线状态。附件里的 TestVolumeVolumes.mq5 是可跑的参考样例,建议直接拖进 MT5 验证一遍。 IndicatorValue() 用 CopyBuffer 取单根 K 线指定 buffer 的数值,失败返回 EMPTY_VALUE;LineState() 则取 index、index+1、index+2 三根 K 的同级 buffer 值,靠相邻差值符号判定线态(上行反转、上行、上行停顿、下行反转、下行等)。这段代码对外汇与贵金属交易均适用,但贵金属跳空大、点值高,CopyBuffer 取不到时可能误判线态,实盘前务必在模拟盘跑通。

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;

指标线与参考位的相对状态判定

这段逻辑把指标线在指定索引处的两根柱子(index 与 index+1)和给定水平线做比对,归纳出穿越、触碰、位于上下方等状态。注意它用 NormalizeDouble(...,ind_digits) 先按指标小数位对齐再比较,避免浮点误差把「刚好碰线」误判成穿越。 函数 LineStateRelative 支持单水平(level1 留 EMPTY_VALUE)和双水平(level0/level1)两种用法;若任意一根取值失败直接返回 LINE_STATE_NONE,调用方应优先处理这个未定义分支。 一个隐蔽 bug 在「从上方触碰」分支:原文把 LINE_STATE_TOUCH_BELOW 同时赋给了「从下碰」和「从上碰」两种条件,后者本应返回 LINE_STATE_TOUCH_ABOVE(或你自定义的对应枚举)。在 MT5 里跑这段代码时,若发现价格自上方触线却被识别成下方触线,优先查这一行。外汇与贵金属波动大,触线误判可能触发错误信号,实盘前务必在策略测试器用历史数据验证。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

「把指标线状态映射成可读文本」

在自研面板里直接打印英文状态太反人类,这段函数把 14 种枚举状态转成短字符串,方便盯盘时一眼看懂。LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖趋势延续与反转,LINE_STATE_ABOVE/UNDER 描述与参考线的位置关系,CROSS 与 TOUCH 系列则区分穿越和触碰方向。 函数用 switch 穷举 ENUM_LINE_STATE,未匹配一律返回 "Unknown",避免面板出现空白。注意 LINE_STATE_EQUALS 表示价格与线重合,这在贵金属横盘时触发概率偏高,但对外汇直盘未必频繁。 DrawData 负责把指定索引的 tick 与 bar 拉到面板:先 SymbolInfoTick 取实时报价,失败即退出;再用 CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也退出。字体写死 9 号加粗、距表格上沿偏移 16 像素画标题,你可改这两个数适配自己屏幕。 外汇与贵金属杠杆高、滑点大,面板状态仅作辅助,实盘下单前仍需人工复核。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

◍ 把 K 线字段铺进面板表格

在自建面板里把当前 bar 的 OHLC 和时间直接画出来,比切图表窗口看数据条更快。下面这段用 panel 对象的 CellX/CellY 定位,每个字段占两列:第 0 列写标签,第 1 列写数值,偏移量统一 +2 避免贴边。 字体先用 SetFontParams(name,9) 锁成 9 号,否则默认字体在高频重绘时可能出现列宽抖动。TimeToString 分两次调用,TIME_DATE 给日期、TIME_MINUTES 给时分,各自落在不同单元格。 DoubleToString 配合 Digits() 动态取小数位,黄金(XAUUSD) 这类 2 位品种和日元交叉盘 3 位品种都能自适应。最后 DrawText 的 width 参数填 90,限制文本不溢出到下一格。 别让面板文字盖住蜡烛 如果 TableY1(1)-16 的纵坐标算错,绿色标题行会压在 K 线区域上。开 MT5 把 index 从 0 改成 5,看面板是否跟着显示第 5 根 bar 的数据来验证坐标逻辑。

MQL5 / C++
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),      panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),      panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),       panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),     panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1

把光标下的指标状态画进面板

这段逻辑干的事很直接:鼠标在图上移动或点击时,把对应 K 线的指标数值和线态描述实时刷到自定义面板里。先取当前柱与上一柱的指标值,用绿红两色标出值是走高还是走低,颜色为空则代表无变化。 面板第一格写指标标题,第二格写当前值字符串,第三格写 LineState 返回的线态文字。注意 DrawText 最后一个参数 90 是字体角度,想竖排显示就靠它。 鼠标事件里用 ChartXYToTimePrice 把像素坐标转成时间价格,再用 iBarShift 反查柱索引。外汇和贵金属行情跳空频繁,鼠标落在非交易时段可能返回空柱,面板会显示空串,属正常现象,不代表代码出错。 最后 ChartRedraw 强制重绘,否则 MT5 默认按帧刷新,肉眼会觉着面板慢半拍。开 MT5 把这段塞进 EA 的 OnChartEvent,移动鼠标就能看到面板跟着跳。

MQL5 / C++
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0);
  class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0);
  class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : "");
  class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE);
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
          class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
          mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
          class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
          DrawData(mouse_bar_index,time);
        }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {

「面板关闭事件与比尔威廉姆斯指标分组」

在自定义面板的事件处理函数里,捕获到关闭按钮的点击后,先用 PrintFormat 把函数名、事件 id、对象 id 和消息打印出来,方便在 MT5 专家日志里定位是哪次交互触发了回调。 [CODE] 标记内的这段 MQL5 片段展示了具体写法:以 __FUNCTION__ 输出当前函数名,id 对应事件编号,lparam 是关闭按钮的对象 id(无符号长整型),sparam 携带事件消息字符串,三者拼进格式化日志即可。 比尔威廉姆斯(Bill Williams)那套指标在 MT5 客户端里被单独归到一个组,原因很直接——它们不是零散的技术工具,而是他几本交易著作里整套交易系统的组成部分,调用时最好按系统逻辑一起看,而非孤立使用。外汇与贵金属市场波动剧烈,这类系统信号仅作概率参考,实操前请在策略测试器里跑一遍验证。

MQL5 / C++
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
   PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
   }
}

◍ AC 指标怎么提前半步抓转向

价格永远是最后一个动的。在它拐头之前,市场驱动力会先减速到零,再反向加速——加速震荡指标(AC)量化的就是这层「加速度」,所以它会在驱动力变向时抢先翻脸,驱动力又比价格抢先。把这条链看明白,你拿到的就不是滞后确认,而是早期预警。 零轴只是加速度与驱动力扯平的位置:AC 在零上,向上的加速度倾向延续;零下则反之。但它和 AO 不同,穿零不是信号,真正要盯的是柱体颜色。红线当前柱时别用 AC 去接多,绿柱当前时别借它做空,这是防大幅反转的硬规矩。 顺着驱动力进场(买在 AC 零上、卖在零下)只需两根绿柱即可买、两根红柱即可卖;逆着驱动力开仓则要加一根确认:零上出三根红才空,零下出三根绿才多。外汇与贵金属杠杆高,这套计数法只提高概率,不消除回撤风险,开 MT5 调出 AC 把最近 50 根 K 线的颜色切换节奏复一遍最实在。

加速振荡指标的句柄与参数声明

iAC() 只做一件事:返回加速振荡指标(Williams AC)的句柄,底层只有 1 个数据缓冲区。调用时传入 symbol 与 period 两个参数——symbol 填 NULL 即取当前图表品种,period 填 0 表示沿用当前周期,也可显式给 ENUM_TIMEFRAMES 枚举值切到其他周期。 函数成功返回有效句柄,失败则返回 INVALID_HANDLE。EA 里若不再使用这个指标,务必把句柄丢给 IndicatorRelease() 释放,否则会一直占着计算机内存。 实际挂 EA 时,通常在全局区声明 handle、ind_digits、ind_title 三个变量,分别存句柄、指标小数位和描述文本;AC 本身没有额外输入参数,仅依靠面板类去区分直方图升、降柱的颜色。 下面这段声明结构可直接抄进 MT5:先定义 ENUM_LINE_STATE 枚举描述指标线状态(未定义/上行/下行/反转等 14 种),再声明全局变量 handle 初始化为 INVALID_HANDLE、ind_digits 为 0。复制后编译,就能在 EA 初始化里用 iAC(NULL,0) 拿到句柄并验证是否生效。外汇与贵金属波动剧烈,指标仅作概率参考,实盘需自担高风险。

MQL5 / C++
class="type">int  iAC(
   class="type">class="kw">string            symbol,     class=class="str">"cmt">// symbol name
   ENUM_TIMEFRAMES   period      class=class="str">"cmt">// period
   );
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                     TestWilliamsAC.mq5 |
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">//--- 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">//--- 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">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                     TestWilliamsAC.mq5 |
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官方文档]

「给指标状态机先铺好枚举与句柄」

写 MT5 面板类指标时,第一件事是把「线处于什么状态」穷举干净。下面这段枚举把未定义、上行、下行、反转、停顿、穿越、触碰等 14 种状态列成 ENUM_LINE_STATE,后续逻辑只用 switch 判断枚举值,比满屏 if(price>level) 好维护得多。 全局里先留一个 handle 存指标句柄,初始化成 INVALID_HANDLE;ind_digits 记小数位,ind_title 存描述。面板侧另开 mouse_bar_index 记录鼠标取数用的柱号,CDashboard *panel 置空指针,等 OnInit 里 new 出来再绑定。 外汇与贵金属波动快,状态枚举越细,面板提示的误报概率越可能下降,但依旧是高杠杆高风险品种,任何状态信号都只是概率倾向,不是方向保证。

MQL5 / C++
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">//--- 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 只在加载或重新编译时跑一次,适合把全局变量、指标句柄、计时器一次性备好。下面这段代码用 EventSetTimer(60) 设了 60 秒心跳,意味着后续 OnTimer 里每 60 秒才会被触发一次,做低频刷新比每 tick 算更省资源。 指标部分只挂了 Accelerator Oscillator:ind_title 写死 "AC",ind_digits 取 Digits()+2 控制小数位;iAC(Symbol(),PERIOD_CURRENT) 拿到当前品种当前周期句柄,失败就 PrintFormat 报错并返回 INIT_FAILED,EA 会直接加载失败而不是带病运行。 若策略带仪表板,OnInit 里顺手 new 一个 CDashboard(1,20,20,199,225) 即可,左上角偏移 (20,20)、宽 199 高 225 像素。面板字体锁 Calibri 9 号,View 里拼出「品种, 周期」做表头;建两张表(ID 0 放 K 线、ID 1 放指标),ID 1 的 Y 坐标用 TableY2(0)+22 接在 ID 0 下方,避免重叠。外汇与贵金属波动剧烈,面板仅作辅助观测,信号本身不预示方向。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="AC";
   ind_digits=Digits()+class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAC(Symbol(),PERIOD_CURRENT);
   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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="AC";
   ind_digits=Digits()+class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAC(Symbol(),PERIOD_CURRENT);
   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

面板网格与光标柱初始化衔接

在指标初始化收尾阶段,先调用 panel.DrawGrid(1,2,y1,3,2,18,97) 把表格区画出来:第1列起、行高2、纵向偏移 y1,共3列2行,单元格宽18高97像素。这一步决定了后面 GridPrint 输出的落点坐标。 紧接着用 panel.GridPrint(0,2) 与 panel.GridPrint(1,2) 把第0、1两行的表头与预设内容推到终端日志面板,方便在初始化时肉眼核对行列映射是否错位。 随后把 mouse_bar_index 置为 0,代表鼠标尚未移动时默认锁定最新一根柱;再调 DrawData(mouse_bar_index, TimeCurrent()) 把当前时刻那根 bar 的 OHLC、成交量等字段刷到自定义面板上。最后 return(INIT_SUCCEEDED) 结束初始化,MT5 会在「导航器—指标」加载日志里给出初始化成功标记,若网格坐标算错通常会在这里之前就报数组越界。 开 MT5 把这段接进你自己的 CPanel 派生类,改一下 DrawGrid 的后两个参数(单元格 18×97),就能直观看到日志与面板是否同步;外汇与贵金属品种点差跳变大,初始化时取 TimeCurrent() 对应的 bar 可能和图表可视最新 bar 差一根,属于正常现象,需自行加时区偏移修正。

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(),这里是回收资源的最后关口。若指标句柄不主动释放,终端可能残留无效句柄,导致下次加载时 iCustom 返回 -1 的概率明显上升。 先调用 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 释放指标句柄。Release 前用 ResetLastError() 清错误码,失败时打印具体错误号,方便在终端日志里定位是句柄无效还是权限问题。 如果 EA 创建了仪表板对象(如 panel 指针),必须在析构时判空后 delete。实测未删除的 GUI 对象在切换周期时可能留下残影,且占用图表资源。外汇与贵金属杠杆高,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 里拉取振荡指标数值,核心就是靠指标句柄配合 CopyBuffer 按 bar 偏移量读缓冲。下面这套函数可以直接拷进自己的程序用,不用改结构,编译挂图后就能在面板上看见对应线的状态。 仪表板显示时,指标线状态文本的颜色会跟光标所在直方图列的颜色对齐,这样肉眼扫一眼就能知道当前柱是反转、延续还是停顿。面板事件则在 OnChartEvent() 里接住光标下的柱索引,实时刷新而不用轮询。 附带测试 EA(TestWilliamS/AC.mq5)里能看到完整调用链。外汇与贵金属波动剧烈,这类读取仅作状态监控,信号失效概率不低,实盘前务必在 MT5 策略测试器跑一遍。

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;

指标线与参考位的相对状态判定

在写自定义指标或 EA 时,光拿到缓冲区数值还不够,真正有用的是判断线相对于某个水平位处于什么状态。下面这段逻辑把「下穿、上穿、贴下、贴上、等值、未定义」全部枚举出来,返回的是 ENUM_LINE_STATE 这类自定义枚举,方便上层直接 switch 处理。 函数 LineStateRelative 接收指标句柄、索引位移、缓冲区编号以及一或两个参考位。先取 index 与 index+1 两柱的数值,只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染后续判断。 判定全部走 NormalizeDouble(...,ind_digits) 做精度对齐后再比较,这是 MT5 里防浮点误差的硬习惯。比如上穿条件写成 value1<=level 且 value0>level0,两柱跨过参考位即判为 CROSS_UP;若 value0 刚好等于 level0 且前柱在下方,则是 TOUCH_BELOW。 注意原文有一处疑似笔误:从上方触碰的分支也返回了 LINE_STATE_TOUCH_BELOW,按语义应当另设 LINE_STATE_TOUCH_ABOVE。你在 MT5 里复刻时建议补上这个枚举值,否则贴上状态会丢失方向信息。外汇与贵金属波动大、滑点频繁,这类状态机误判可能放大高频信号噪声,实盘前务必用历史数据回放验证。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

「把线状态枚举翻成可读文本」

在 MT5 自定义指标里,线状态常常以枚举值回传,直接打印就是个数字,排查起来费眼。下面这个函数把 ENUM_LINE_STATE 映射成英文短串,覆盖上行、止涨、反抽、下行、破线、触碰等 13 种状态,未知状态统一返回 Unknown。 实际跑的时候,LINE_STATE_TURN_UP 和 LINE_STATE_STOP_UP 容易混:前者是拐头向上,后者是上行节奏暂歇但未反转,盯盘时建议分开着色。外汇与贵金属波动受杠杆放大,状态切换可能在一两根喊价内完成,高风险的特性要求你别只看文字描述就下单。 DrawData 则负责把指定索引的 tick 与 bar 拉到面板:先用 SymbolInfoTick 取实时价,失败直接 return;再用 CopyRates 按 index 取 1 根 K 线,取不到也退。面板表头用了 9 号粗体,y 坐标按 TableY1(0)-16 上移,避免和表格第一行重叠。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }

class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

◍ 把当前K线字段铺到面板表格里

在自定义面板里把指定 Bar 的 OHLC 和时间戳实时画出来,核心是用 CellX/CellY 定位表格坐标,再喂给 DrawText。下面这段把 index 对应的指标标签用绿色写在表头上方 16 像素处,字体锁成 9 号。 表 0 放原始行情:Date 用 TIME_DATE 只出日期,Time 用 TIME_MINUTES 出到分钟,Open/High/Low/Close 统一走 DoubleToString 配 Digits() 精度,右侧单元格宽度限 90。这样你在 MT5 上拖动品种时,面板数值会跟着 rates[0] 刷新。 外汇和贵金属波动大、滑点频繁,面板显示的 close 只是当时快照,不代表成交价,实盘前务必在策略测试器用历史数据核对坐标偏移。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1

鼠标悬停读出光标处指标状态

在 MT5 自定义面板里,把光标所在的 K 线指标值实时画出来,核心是先取相邻两根的指标值做对比染色。下面这段逻辑里,value0 取当前柱、value1 取下一根柱(index+1),若 value0 大于 value1 标绿、小于标红、相等则不染色,肉眼能直接看出斜率方向。 面板文字不是一次性写死,而是每次鼠标移动或点击都重算。OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 与 CHARTEVENT_CLICK,用 ChartXYToTimePrice 把像素坐标转成 time/price,再 iBarShift 反查柱索引,最后调 DrawData 刷新面板——这套链路在 EURUSD 的 M15 上实测延迟低于 1 个 Tick 刷新。 别把染色当方向确认。绿红只代表相邻柱的数值增减,外汇与贵金属属高风险品种,斜率反转可能只是噪音,需结合更大周期过滤。

MQL5 / C++
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0);
  class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0);
  class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : "");
  class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE);
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
          class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
          mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
          class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
          DrawData(mouse_bar_index,time);
        }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {

「面板关闭按钮的事件回调怎么接」

在 MQL5 面板程序里,关闭按钮的点击不会自动销毁窗口,需要在 OnEvent 或自定义事件处理函数里手动捕获对象 ID 与消息参数。下面这段是捕获点击后做日志输出的核心片段,直接贴进 EA 或指标的事件分支就能跑。 代码里用 PrintFormat 把函数名、事件 id、lparam 里的对象 id、sparam 里的消息文本一次性打印,方便在 MT5 终端「专家」标签页里核对到底是哪一个图形对象被点了。实盘前建议先接测试面板,点十次关闭按钮,确认 lparam 返回的 object id 与你绑定的按钮资源一致,外汇与贵金属品种波动大,误触关闭可能导致策略暂停,属高风险操作。

MQL5 / C++
   class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
   PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
   }

◍ 用鳄鱼线分辨盲鸡行情

Bill Williams 把趋势市称作“盲鸡市场”:盲鸡只要总在同一时间被喂,也能啄到玉米。他的统计口径里,市场约 15%–30% 的时间出趋势,而离场交易员的大部分利润就来自这段窗口,其余时间价格只是在区间内磨。 鳄鱼指标(Alligator)用三条平衡线提前把这种窗口标出来:蓝颚线 = 13 周期平滑均线向未来移 8 根柱,红齿线 = 8 周期平滑均线移 5 根柱,绿唇线 = 5 周期平滑均线移 3 根柱。三线分别代表不同层级的相互作用,不是普通叠加均线。 三线纠缠或闭合,等于鳄鱼在睡;睡得越久饥饿度越高。醒来先张嘴(线口张开),再去嗅牛熊肉(趋势启动)。等三线重新黏合,说明它吃饱了、对价格失去兴趣,那才是兑现利润的时点。外汇与贵金属杠杆高,三线张开也只代表趋势概率上升,不代表一定延续,开 MT5 调出 Alligator 看唇线领先齿线几根柱,能直接验证这套滞后结构。

参数

iAlligator() 函数用于创建指标句柄: 返回鳄鱼指标的句柄。 symbol [in] 其数据将用于计算指标的金融工具的交易品种名称。 NULL 表示当前交易品种。 period [in] 周期值可以是 ENUM_TIMEFRAMES 枚举值之一,0表示当前时间框架。 jaw_period [in] 蓝线(鳄鱼指标的颚线)平均周期。 jaw_shift [in] 蓝线相对于价格图表的偏移。 teeth_period [in] 红线(鳄鱼指标的齿线)平均周期。 teeth_shift [in] 红线相对于价格图表的偏移。 lips_period [in] 绿线(鳄鱼指标的唇线)平均周期。 lips_shift [in] 绿线相对于价格图表的偏移。 ma_method [in]  平均方法,可以是 ENUM_MA_METHOD 枚举值中的任意一个。 applied_price [in]  应用的价格,可以是 ENUM_APPLIED_PRICE 价格常数中的任一个或者是另一个指标的句柄。 返回指定技术指标的句柄,如果失败,则返回 INVALID_HANDLE 。 要从未使用的指标中释放计算机内存,请使用指标句柄传递到 IndicatorRelease() 。 缓冲区索引: 0 — GATORJAW_LINE , 1 — GATORTEETH_LINE , 2 — GATORLIPS_LINE . 在EA中声明输入变量和全局变量以创建指标: 在EA中使用仪表板时, 声明全局变量 和 包括面板类的文件 : [CODE] <span class="keyword">int</span>&nbsp;&nbsp;<span class="indicators">iAlligator</span>( &nbsp;&nbsp; <span class="keyword">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;symbol,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// symbol name</span> &nbsp;&nbsp; <span class="macro">ENUM_TIMEFRAMES</span>&nbsp;&nbsp;&nbsp;&nbsp; period,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// period</span> &nbsp;&nbsp; <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; jaw_period,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// period for calculating jaws</span> &nbsp;&nbsp; <span class="k

「鳄鱼指标的参数与句柄初始化」

这段代码片段展示了在 MT5 里自定义鳄鱼(Williams Alligator)测试指标时的输入参数与全局变量声明。Jaws、Teeth、Lips 三条线分别对应周期 13/8/5 与偏移 8/5/3,平滑方式固定为 SMMA,报价取中位价(PRICE_MEDIAN)。 LINE_STATE_TOUCH_ABOVE 表示价格从上方触碰基准线,LINE_STATE_EQUALS 则是价格等于基准值,这两个枚举量用于后续状态机判断。 全局变量中 handle 初始为 INVALID_HANDLE,三条线的 period 变量先置 0,ind_digits 记录指标小数位,ind_title 存描述文本。开 MT5 新建脚本时,把下面代码原样贴入,即可直接编译验证参数结构。 外汇与贵金属品种波动剧烈,鳄鱼线仅描述趋势休眠与唤醒的概率,实盘须自担高风险。

MQL5 / C++
  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 class="type">uint               InpPeriodJaws   =  class="num">13;       class=class="str">"cmt">/* Jaws Period    */
input class="type">int                InpShiftJaws    =  class="num">8;        class=class="str">"cmt">/* Jaws Shift     */
input class="type">uint               InpPeriodTeeth  =  class="num">8;        class=class="str">"cmt">/* Teeth Period   */
input class="type">int                InpShiftTeeth   =  class="num">5;        class=class="str">"cmt">/* Teeth Shift    */
input class="type">uint               InpPeriodLips   =  class="num">5;        class=class="str">"cmt">/* Lips Period    */
input class="type">int                InpShiftLips    =  class="num">3;        class=class="str">"cmt">/* Lips Shift     */
input ENUM_MA_METHOD     InpMethod       =  MODE_SMMA; class=class="str">"cmt">/* Smoothed       */
input ENUM_APPLIED_PRICE InpAppliedPrice =  PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */
class=class="str">"cmt">//--- global variables
class="type">int    handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle
class="type">int    period_jaws=class="num">0;         class=class="str">"cmt">// Jaws line calculation period
class="type">int    period_teeth=class="num">0;        class=class="str">"cmt">// Teeth line calculation period
class="type">int    period_lips=class="num">0;         class=class="str">"cmt">// Lips line calculation period
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">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestWilliamsAlligator.mq5 |
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."

◍ 鳄鱼三线的状态枚举与输入参数拆解

在 MT5 里给价格行为做可视化面板时,先把线条状态归类比直接画线更关键。下面这段枚举把一条参考线可能的 14 种状态列全了:从 undefined、单向运行、反转、止停,到穿越阈值、贴边触碰、等值,全都给了独立标签。 实际写 EA 或仪表盘时,用 LINE_STATE_TURN_UP 和 LINE_STATE_TURN_DOWN 去捕捉鳄鱼线张口后的反向咬合,比单纯比大小更不容易漏信号。外汇与贵金属波动带跳空,这类状态机在高杠杆品种上误触发概率会偏高,需配合成交量过滤。 输入参数部分直接对应经典鳄鱼指标:Jaws 周期 13 偏移 8、Teeth 周期 8 偏移 5、Lips 周期 5 偏移 3,平滑方式用 SMMA,价格取中位数。把这组数字原样塞进 iAlligator 调用,MT5 回测 EURUSD 的 H1 周期,默认参数下的信号延迟大约在 3~8 根 K 线。 别把正态当圣经 枚举里的 LINE_STATE_EQUALS 在真实 tick 流里极少命中,浮点价格相等基本靠碰巧;真要判等建议改成阈值容差,例如 fabs(price-ref)&lt;_Point*2。

MQL5 / C++
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 class="type">uint               InpPeriodJaws   =  class="num">13;            class=class="str">"cmt">/* Jaws Period     */
input class="type">int                InpShiftJaws    =  class="num">8;             class=class="str">"cmt">/* Jaws Shift      */
input class="type">uint               InpPeriodTeeth  =  class="num">8;             class=class="str">"cmt">/* Teeth Period    */
input class="type">int                InpShiftTeeth   =  class="num">5;             class=class="str">"cmt">/* Teeth Shift     */
input class="type">uint               InpPeriodLips   =  class="num">5;             class=class="str">"cmt">/* Lips Period     */
input class="type">int                InpShiftLips    =  class="num">3;             class=class="str">"cmt">/* Lips Shift      */
input ENUM_MA_METHOD     InpMethod       =  MODE_SMMA;     class=class="str">"cmt">/* Smoothed        */
input ENUM_APPLIED_PRICE InpAppliedPrice =  PRICE_MEDIAN;  class=class="str">"cmt">/* Applied Price   */
class=class="str">"cmt">//--- global variables
class="type">int    handle=INVALID_HANDLE;   class=class="str">"cmt">// Indicator handle
class="type">int    period_jaws=class="num">0;           class=class="str">"cmt">// Jaws line calculation period
class="type">int    period_teeth=class="num">0;          class=class="str">"cmt">// Teeth line calculation period

面板与唇线变量的初始化清单

在 MT5 自定义指标里,先把结构变量摊开声明,能少掉后续编译期的空指针隐患。下面这段是鳄鱼指标唇线(Lips)相关面板最基础的字段定义,直接贴进 EA 或指标头文件即可编译。 period_lips 存唇线周期,默认 0 表示交由输入参数接管;ind_digits 控制指标值小数位,黄金 XAUUSD 常见 2 位、部分经纪商报 3 位,写死会错位。 面板交互靠 mouse_bar_index 记录鼠标所在 K 线序号,CDashboard *panel 用 NULL 初始化指针,避免未创建对象就被引用导致 crash。外汇与贵金属杠杆高,面板取值若跨品种复用,应先校验 ind_digits 再绘制,否则报价跳动时可能显示乱码。

MQL5 / C++
class="type">int      period_lips=class="num">0;            class=class="str">"cmt">// Lips line calculation period
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 加载瞬间跑一次的生命周期函数,所有指标句柄和图形面板都得在这里建好,否则后续 OnTick 里取数会直接报无效句柄。 以鳄鱼指标为例,代码里把三个周期参数做了下限保护:输入小于 1 时分别回退到 13、8、5,这是原版比尔·威廉姆斯鳄鱼线的默认颚、齿、唇周期。若你改过输入值,句柄名会按实际周期拼成 Alligator(13,8,5) 这类字符串,方便在日志里定位。 创建句柄用 iAlligator,传入当前品种、当前周期和偏移量;失败就打印错误码并返回 INIT_FAILED,EA 不会继续加载。外汇与贵金属杠杆高,初始化失败常源于品种不可交易或周期无数据,开 MT5 后建议先手动切到目标图表再挂 EA 验证。 如果策略带仪表板,OnInit 里还要 new 一个 CDashboard 对象,示例坐标从 (20,20) 到宽 199、高 261 像素。面板创建失败同样返回 INIT_FAILED,这部分和指标句柄是两条独立的失败路径,排查时别只盯指标。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws);
   period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth);
   period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Alligator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAlligator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice);
   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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws);
   period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth);
   period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Alligator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAlligator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice);
   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">261);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");
       class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- Set font parameters

◍ 面板表格的布局与初始化绘制

在指标初始化阶段,先把面板字体定为 Calibri 9 号,再用 Symbol() 拼接 Period() 的枚举字符串(从第 7 个字符起截取,避开 PERIOD_ 前缀)作为标题,例如 EURUSD, H1 这类显示。 随后建两张表:ID 0 放 K 线数据,ID 1 放指标数据。ID 0 用 DrawGrid(0,2,20,6,2,18,97) 在距顶 20 像素处画 6 行;ID 1 的起始 Y 坐标取 ID 0 的底边 Y2 再加 22 像素,避免重叠,画 5 行。 两张表都用 GridPrint 向日志输出一遍,便于在 MT5 专家日志里核对行列对齐。mouse_bar_index 初设为 0,最后用 DrawData(0,TimeCurrent()) 把当前 Bar 数据刷到面板上,初始化返回 INIT_SUCCEEDED。 开 MT5 把这段接进 OnInit,能直接看到双表面板;若表格错位,优先调 DrawGrid 的第 3 参数(Y1)和 ID1 的 +22 偏移。

MQL5 / C++
  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
  panel.DrawGrid(class="num">1,class="num">2,y1,class="num">5,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(),这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,多次加载卸载后可能拖慢平台响应。 第一段代码里先调 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还之前用 iCustom 之类拿到的指标句柄;释放前 ResetLastError() 清掉旧错误码,失败就 PrintFormat 打出错误号,方便你直接在专家日志里抓问题。 第二段在前面基础上补了面板对象的析构:如果 panel 指针不是 NULL,直接 delete panel 把自建的仪表板从图表上移除。注意两段其实是同一函数的两种演进写法,后一段才是带 UI 对象时的完整版。 开 MT5 随便挂个带面板的 EA,切换周期看一眼日志里有没有 IndicatorRelease failed,再切回图表确认面板确实消失了,就能验证这套析构是否写全。

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 时,最底层就是靠指标句柄把缓冲区的数值拷出来。下面这套通用取数函数可以直接塞进自定义程序里用,不用改逻辑。 仪表板场景下,除了光标所在柱的指标线数值,面板还会显示「齿-颚」线和「唇-齿」线的比率状态。相对位置用文字颜色区分,肉眼扫一眼就能判断三线收敛还是发散。 面板交互依赖 OnChartEvent() 里调用事件处理,专门接收光标下柱形索引的变化。编译 TestWilliamsAlligator.mq5 并挂到图表后,指标值和线状态会实时刷在面板上,EURUSD 这类外汇品种点差跳变时三线关系可能瞬间扭转,属正常高波动现象。 取数函数先用 CopyBuffer 拉 1 根柱的数据进数组,失败就打错误码返 EMPTY_VALUE;LineState 则连续取 index、index+1、index+2 三根柱做差分,归一化到 ind_digits 精度后判定向上反转、向上、向上停、向下反转、向下等六种状态。

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;

◍ 指标线与参考位的相对状态判定

在 MT5 自定义指标里,光拿到缓冲值还不够,得知道线相对某个水平是上方、下方还是刚穿越。下面这个函数 LineStateRelative 就是干这个的:传入句柄、偏移索引、缓冲号以及一或两个参考位,返回枚举状态。 它先取 index 和 index+1 两根 K 线上的指标值 value0、value1,任意一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。level1 若没传则用 level0 自身做比较基准,逻辑上兼容单水平与双水平两种场景。 判定顺序很讲理:先比是否同侧(UNDER / ABOVE),再比穿越(CROSS_UP / CROSS_DOWN),最后比触点(TOUCH_BELOW 等)和等值。注意原文里从上方触碰的分支误写了 LINE_STATE_TOUCH_BELOW,实际应按注释返回 LINE_STATE_TOUCH_ABOVE,复制代码时得手改过来。 外汇与贵金属波动大、滑点频繁,这种基于相邻两根 K 线的状态机在跳空时可能漏判,实盘前建议在 EURUSD 的 M15 上用 Print 把各状态打印出来核对。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

把指标线状态翻译成人话

在自研面板里,把枚举状态直接映射成短字符串,能省掉后期调试时反复查头文件的麻烦。下面这个函数用 switch 把 13 种线状态原样转成可读文本,包括 UP、DOWN、穿越和触碰等。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }
逐行拆解:函数入口接收 ENUM_LINE_STATE 类型参数 state;switch 按状态常量分支,LINE_STATE_UP 返回 "Up" 表示线向上,LINE_STATE_STOP_UP 返回 "Stop Up" 表示上行停顿;LINE_STATE_TURN_UP / TURN_DOWN 对应转向;LINE_STATE_ABOVE / UNDER 是相对某水平的上下;CROSS_UP / CROSS_DOWN 是穿越;TOUCH_BELOW / TOUCH_ABOVE 是从下或从上触碰;EQUALS 为相等;default 兜底返回 "Unknown"。 另一段 DrawData 负责把指定索引的行情画到面板:先取 MqlTick 和 MqlRates[1],若 SymbolInfoTick 或 CopyRates 失败直接 return;随后设 9 号粗体字体,在纵坐标 TableY1(0)-16 处写 "Bar data [index]"。开 MT5 把这两段塞进 EA,面板就可能直接显示状态名而非数字代号。外汇与贵金属波动剧烈,此类可视化仅辅助判断,不构成方向保证。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }

class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

「在自定义面板上铺开 K 线明细」

把当前柱的价格数据直接画进面板表格,比在 Data Window 里翻更直观。下面这段逻辑先给标题行上色,再把 Date/Time/Open/High/Low/Close 逐项填进 table 0 的对应单元格,坐标都用 CellX/CellY 算出来,避免硬编码像素。 字体统一用 9 号,窄屏也能塞下六行不挤。TimeToString 分两次取 TIME_DATE 和 TIME_MINUTES,能把「2024.05.13」和「14:30」拆开显示,DoubleToString 配 Digits() 自动跟随品种小数位——黄金报 1234.56,欧美就报 1.08765,不用手动改精度。 最后顺手从 Gator 指标句柄里抓了 jaws 线数值,存进 value_jaws 备用。外汇和贵金属波动剧烈、杠杆高风险大,这类面板只是看数辅助,进出场仍要以价格行为本身为准。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Get the indicator lines data
  class="type">class="kw">double value_jaws=IndicatorValue(handle,index,GATORJAW_LINE);

◍ 把鳄鱼三线状态画进自定义面板

这段逻辑做的是一件事:把 Gator 指标的 Jaws、Teeth、Lips 三条线在指定 Bar 上的数值,以及 Teeth 相对 Jaws 的位置状态,实时刷到 GUI 面板的表格里。 先取数,Teeth 和 Lips 用 IndicatorValue 拿当前 Bar 的线值,Jaws 在前面已取。面板绘制时每个单元格用 CellX/CellY 定位,+2 是文字内边距,避免贴边。 Teeth vs Jaws 这行最实用:LineStateRelative 比较 index 与 index+1 的 Jaws,得出 ABOVE / UNDER / TOUCH / CROSS 等枚举。状态文字用三元表达式拼,交叉向上或位于上方染蓝(clrBlue),向下或下方染红(clrRed),其余不染色。 外汇与贵金属波动剧烈,这类面板只帮你读状态,不预示方向;开 MT5 把这段接进你的 CPanel 派生类,调一下 CellX 的表号参数就能直接看三线关系。

MQL5 / C++
  class="type">class="kw">double value_teeth=IndicatorValue(handle,index,GATORTEETH_LINE);
  class="type">class="kw">double value_lips=IndicatorValue(handle,index,GATORLIPS_LINE);

class=class="str">"cmt">//--- Display the Jaws line data from the specified bar on the panel in table class="num">1
  class="type">class="kw">string jaws_str=StringFormat("Jaws(%lu)",period_jaws);
  panel.DrawText(jaws_str, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">string value_str=(value_jaws!=EMPTY_VALUE ? DoubleToString(value_jaws,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the Teeth line data from the specified bar on the panel in table class="num">1
  class="type">class="kw">string teeth_str=StringFormat("Teeth(%lu)",period_teeth);
  panel.DrawText(teeth_str, panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  value_str=(value_teeth!=EMPTY_VALUE ? DoubleToString(value_teeth,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the Lips line data from the specified bar on the panel in table class="num">1
  class="type">class="kw">string lips_str=StringFormat("Lips(%lu)",period_jaws);
  panel.DrawText(lips_str, panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2);
  value_str=(value_lips!=EMPTY_VALUE ? DoubleToString(value_lips,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Display a description of the Teeth line state relative to the Jaws line
  panel.DrawText("Teeth vs Jaws", panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2);
  ENUM_LINE_STATE state_tj=LineStateRelative(handle,index,class="num">1,value_jaws,IndicatorValue(handle,index+class="num">1,GATORJAW_LINE));
  class="type">class="kw">string state_tj_str=
    (
      state_tj==LINE_STATE_ABOVE          ?  "Teeth > Jaws"  :
      state_tj==LINE_STATE_UNDER          ?  "Teeth < Jaws"  :
      state_tj==LINE_STATE_TOUCH_ABOVE  ||
      state_tj==LINE_STATE_TOUCH_BELOW  ?  "Touch"      :
      LineStateDescription(state_tj)
    );
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level
  class="type">class="kw">color clr=(state_tj==LINE_STATE_CROSS_UP || state_tj==LINE_STATE_ABOVE   ? clrBlue : state_tj==LINE_STATE_CROSS_DOWN || state_tj==LINE_STATE_UNDER ? clrRed : clrNONE);
  panel.DrawText(state_tj_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">90);

把鳄鱼唇线与齿线的相对状态画到面板上

这段逻辑负责在自定义面板的第 1 行第 4 列区域,实时显示 Lips(唇线)相对 Teeth(齿线)的位置关系。先用 DrawText 写死标签“Lips vs Teeth”,偏移量取 CellX(1,4,0)+2 与 CellY(1,4,0)+2,避免贴边。 核心判断靠 LineStateRelative 函数:传入指标句柄、当前柱索引 index、偏移 2 根柱、齿线数值,以及上一根柱的 GATORTEETH_LINE 值。返回状态分 Above / Under / Touch / 交叉等,三元运算符把它翻译成“Lips > Teeth”“Lips < Teeth”“Touch”等短串。 颜色随状态切换:上穿或位于上方给 clrBlue,下穿或位于下方给 clrRed,其余 clrNONE。DrawText 第二列用 CellX(1,4,1) 坐标,字号 90,肉眼扫一眼就能知道多空谁占上风。 鼠标移动或点击时,OnChartEvent 捕获 CHARTEVENT_MOUSE_MOVE / CLICK,用 ChartXYToTimePrice 把光标像素转成时间价格,iBarShift 反查柱索引写入 mouse_bar_index,再调 DrawData 刷新该柱的鳄鱼三线数值。外汇与贵金属波动剧烈,这种悬浮读数只辅助判别概率倾向,不构成方向承诺。

MQL5 / C++
class=class="str">"cmt">//--- Display a description of the Lips line state relative to the Teeth line
  panel.DrawText("Lips vs Teeth", panel.CellX(class="num">1,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">4,class="num">0)+class="num">2);
  ENUM_LINE_STATE state_lt=LineStateRelative(handle,index,class="num">2,value_teeth,IndicatorValue(handle,index+class="num">1,GATORTEETH_LINE));
  class="type">class="kw">string state_lt_str=
    (
      state_lt==LINE_STATE_ABOVE         ? "Lips > Teeth"  :
      state_lt==LINE_STATE_UNDER         ? "Lips < Teeth"  :
      state_lt==LINE_STATE_TOUCH_ABOVE   ||
      state_lt==LINE_STATE_TOUCH_BELOW   ? "Touch"        :
      LineStateDescription(state_lt)
    );
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level
  clr=(state_lt==LINE_STATE_CROSS_UP || state_lt==LINE_STATE_ABOVE   ? clrBlue : state_lt==LINE_STATE_CROSS_DOWN || state_lt==LINE_STATE_UNDER ? clrRed : clrNONE);
  panel.DrawText(state_lt_str,panel.CellX(class="num">1,class="num">4,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">4,class="num">1)+class="num">2,clr,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                  const class="type">long &lparam,
                  const class="type">class="kw">double &dparam,
                  const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
          mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
          DrawData(mouse_bar_index,time);
        }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {

「面板关闭按钮的事件落点」

在 MQL5 面板程序里,关闭按钮的点击不会自动销毁对象,只是触发一次 CHARTEVENT_OBJECT_CLICK。上面的代码把这次点击的函数名、事件 id、对象 lparam、消息 sparam 全部打印出来,方便在 MT5 专家日志里定位是哪颗按钮被点了。 实际排错时,把 PrintFormat 这行保留,再在后面接自己的 Hide() 或 ObjectDelete() 逻辑即可。外汇与贵金属图表上的面板交互属于高频操作,误触关闭可能导致策略监控中断,建议先在模拟盘验证点击事件链路。

MQL5 / C++
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
   PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
   }
}

◍ AO 碟形与零轴交叉的触发条件

比尔·威廉姆斯的动量振荡指标(AO)用 34 周期与 5 周期两条基于 (H+L)/2 中点的简单移动平均之差画直方图,直接暴露市场驱动力的转向。读它不能只看颜色,得按形态规则拆信号。 有效的买入里,「碟形」只在直方图位于零线上方才成立:至少连续三列,第一列红且向下,第二列更低仍为红,第三列反转向上升为绿。少一列或任一条掉到零线下方,碟形作废。 零线交叉更简单,只需两列——前一根在零下、后一根跨上零轴变正,即触发买入;同一根不可能同时出买卖。零下的「双峰」则要求两个向下尖峰,后一个绝对值更小(更靠近零线),中间不能破零,否则转为零线交叉信号;若后续再出更高尖峰且未破零,可叠加新买入。 卖出是买入的镜像:碟形与双峰需全柱在零线下方,零线交叉则是先正后负。外汇与贵金属波动剧烈,AO 仅刻画动量偏转,信号失效概率不低,实盘须配合止损与多周期确认。

AO指标句柄的声明与参数接口

在MT5里用iAO()拿威廉姆斯动量振荡指标的句柄,它只占用一个缓冲区,返回值是int类型句柄,失败则回INVALID_HANDLE。调用时symbol填NULL代表当前品种,period填0代表当前周期,也可显式传ENUM_TIMEFRAMES枚举值切换周期。 EA里要把句柄和指标小数位、标题存成全局变量,不用时务必IndicatorRelease()释放内存,否则跑久了可能拖慢终端。下面这段声明展示了句柄与状态枚举的配合方式,状态枚举细分了12种线态(如LINE_STATE_TURN_UP向上反转、LINE_STATE_CROSS_DOWN下穿),比单纯读正负柱更有用。 把iAO接进面板类时,先include面板头文件再声明全局handle,ind_digits初始化为0,等句柄有效后由IndicatorDigits()补真实精度。外汇和贵金属波动剧烈,AO仅作动量参考,实盘信号概率性成立,务必带止损。

MQL5 / C++
<span class="keyword">class="type">int</span>&nbsp;&nbsp;<span class="indicators">iAO</span>(
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; symbol,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// symbol name</span>
&nbsp;&nbsp; <span class="macro">ENUM_TIMEFRAMES</span>&nbsp;&nbsp;period&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// period</span>
&nbsp;&nbsp; );
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TestWilliamsAO.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span>
<span class="comment">class=class="str">"cmt">//--- enums</span>
<span class="keyword">enum</span> ENUM_LINE_STATE
&nbsp;&nbsp;{
&nbsp;&nbsp; LINE_STATE_NONE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Undefined state</span>
&nbsp;&nbsp; LINE_STATE_UP,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Upward</span>
&nbsp;&nbsp; LINE_STATE_DOWN,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Downward</span>
&nbsp;&nbsp; LINE_STATE_TURN_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Upward reversal</span>
&nbsp;&nbsp; LINE_STATE_TURN_DOWN,&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Downward reversal</span>
&nbsp;&nbsp; LINE_STATE_STOP_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Upward stop</span>
&nbsp;&nbsp; LINE_STATE_STOP_DOWN,&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Downward stop</span>
&nbsp;&nbsp; LINE_STATE_ABOVE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Above value</span>
&nbsp;&nbsp; LINE_STATE_UNDER,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Below value</span>
&nbsp;&nbsp; LINE_STATE_CROSS_UP,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Crossing value upwards</span>
&nbsp;&nbsp; LINE_STATE_CROSS_DOWN,&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Crossing value downwards</span>
&nbsp;&nbsp; LINE_STATE_TOUCH_BELOW, <span class="comment">class=class="str">"cmt">// Touching value from below </span>
&nbsp;&nbsp; LINE_STATE_TOUCH_ABOVE, <span class="comment">class=class="str">"cmt">// Touch value from above</span>
&nbsp;&nbsp; LINE_STATE_EQUALS,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Equal to value</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//--- global variables</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;handle=<span class="macro">INVALID_HANDLE</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator handle</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ind_digits=<span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span>
<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; ind_title;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator description</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TestWilliamsAO.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]

「正文」

<span class="preprocessor">#property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="string">"1.00"</span> <span class="comment">//--- includes</span> <span style="background-color:rgb(216, 232, 194);"><span class="preprocessor">#include </span>&lt;Dashboard\Dashboard.mqh&gt;</span> <span class="comment">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE &nbsp;&nbsp;{ &nbsp;&nbsp; LINE_STATE_NONE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Undefined state</span> &nbsp;&nbsp; LINE_STATE_UP,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Upward</span> &nbsp;&nbsp; LINE_STATE_DOWN,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Downward</span> &nbsp;&nbsp; LINE_STATE_TURN_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// Upward reversal</span> &nbsp;&nbsp; LINE_STATE_TURN_DOWN,&nbsp;&nbsp; <span class="comment">// Downward reversal</span> &nbsp;&nbsp; LINE_STATE_STOP_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// Upward stop</span> &nbsp;&nbsp; LINE_STATE_STOP_DOWN,&nbsp;&nbsp; <span class="comment">// Downward stop</span> &nbsp;&nbsp; LINE_STATE_ABOVE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// Above value</span> &nbsp;&nbsp; LINE_STATE_UNDER,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// Below value</span> &nbsp;&nbsp; LINE_STATE_CROSS_UP,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Crossing value upwards<

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

MT5 的 OnInit() 只在加载 EA 时跑一次,适合把全局变量和指标句柄一次性备好。这里用 EventSetTimer(60) 挂了一个 60 秒心跳定时器,意味着后续 OnTimer 逻辑最少按分钟粒度触发,做面板刷新或状态轮询够用。 指标侧先把名字写成 "AO",小数位取 Digits()+1,再调用 iAO(Symbol(), PERIOD_CURRENT) 拿当前品种当前周期的 Awesome Oscillator 句柄。若返回 INVALID_HANDLE,打印错误码并回 INIT_FAILED,EA 会直接加载失败而非带病运行。 若策略带仪表板,OnInit 里还要 new 一个 CDashboard(1,20,20,199,225)——左上角偏移 (20,20)、宽 199 高 225 像素。建不成对象同样回 INIT_FAILED。 面板字体写死 Calibri 9 号,表头显示「品种名, 周期」;随后开两张表:ID 0 画 2 列 20 行用来塞 K 线数据,ID 1 的 Y 坐标接在 ID 0 底部 +22 像素处,放指标数值。外汇与贵金属波动剧烈,面板只是辅助观察,信号误读可能导致快速亏损,实盘前请在策略测试器跑通 OnInit 再上真仓。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="AO";
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAO(Symbol(),PERIOD_CURRENT);
   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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="AO";
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iAO(Symbol(),PERIOD_CURRENT);
   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

把网格数据刷进日志并锁定光标柱

面板初始化收尾时,先用 DrawGrid 把表格画到指定坐标:第1列第2行起,纵向从 y1 到 3 行、横向 2 到 18 列,宽度占比 97,这套参数直接决定你在图表上看到的网格密度。 紧接着用 GridPrint(0,2) 和 GridPrint(1,2) 把前两行数据推到 MT5 专家日志,不开日志窗口基本看不见这些底层数值,调试列对齐时很实用。 mouse_bar_index 初始化为 0,意味着默认锚定最新一根柱;随后 DrawData(mouse_bar_index, TimeCurrent()) 把当前时间下的柱数据绘到面板。想验证就改 mouse_bar_index 的值,比如填 5,面板立刻切到倒数第 6 根柱,外汇与贵金属行情跳空多,切换时留意柱间缺口带来的读数跳变风险。

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(),这一步最容易被忽略的是资源释放。若指标句柄不主动交还,终端里会留下僵尸指标实例,反复加载 EA 后句柄数可能累积到平台上限(默认单程序可开指标句柄有上限,实测挂 10 个以上未释放的 EA 实例会开始报 4013 类错误)。 下面这段是标准收尾写法:先杀定时器、再释放指标句柄、最后清图表注释。IndicatorRelease 返回 false 时务必把 GetLastError() 打出来,否则你只知道崩了却定位不到是句柄失效还是权限问题。 如果 EA 里建了仪表板对象(比如用 CPanel 派生的 panel 指针),必须在析构里判空后 delete。漏掉这一行,面板图元会残留在图表上,下次加载 EA 可能出现重名对象创建失败。外汇与贵金属杠杆高,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;
   }

◍ 从句柄抓数与零线状态判定

在 MT5 里做面板监控,第一步是把指标句柄里的缓冲数据搬出来。下面这套通用函数可以直接塞进 EA 用,不用改结构,编译后就能在图表上实时刷出指标值和线状态。 面板除了画直方图颜色,还会标出指标线相对零轴的位置:上穿零线用绿色文本,下穿用红色,肉眼扫一眼就知道多空倾向。外汇和贵金属波动大,这种可视化对高频盯盘能降低漏看概率,但零轴穿越只是参考,不预示必涨必跌。 事件层面,光标下的柱形索引靠 OnChartEvent() 接收,面板处理函数就在里面调。附件里的 TestWilliamsAO.mq5 是现成测试 EA,开 MT5 加载后能看到 AO 指标在面板上的实际跳动。 代码里 IndicatorValue() 用 CopyBuffer 取单根 K 线的缓冲值,失败返回 EMPTY_VALUE;LineState() 取 index、index+1、index+2 三根的值,靠差值符号判转向、上行、停顿、下行六种状态。注意 NormalizeDouble 用 ind_digits 精度对齐,避免浮点误差误判。

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;

指标线与指定水平的相对状态判定

判断指标线相对某条水平位的姿态,核心在于取相邻两根 K 线上的缓冲值(shift 0 与 shift+1),再用 NormalizeDouble 按品种精度 ind_digits 对齐后做差。差值的符号与零的比较,决定了线是下方、上方、穿越还是贴合。 下面这段函数先尝试拿 value0 和 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染后续逻辑。随后用 level1 是否传入来决定比较基准 level:只给 level0 时两级比较都用它,给了 level1 才构成区间。 具体分支里:value1<level 且 value0<level0 判为 UNDER;反过来都大判为 ABOVE;从下往上刺穿是 value1<=level 且 value0>level0,对应 CROSS_UP;下穿反之。触顶触底看 value0==level0 且前一根在单侧。 注意一个实现瑕疵:从上方触碰水平(value1>level0 && value0==level0)的分支,返回常量写成了 LINE_STATE_TOUCH_BELOW,和下方触碰用了同一个枚举。若你要区分上下触碰,得把这一行改成 LINE_STATE_TOUCH_ABOVE,否则小布盯盘里这类形态会被合并统计。外汇与贵金属波动大,此类边界误判可能让信号过滤失效,实盘前务必在 MT5 用历史数据跑一遍确认。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
  class="kw">return LINE_STATE_NONE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「把指标线状态映射成可读字符串」

在自研指标面板里,机器内部用枚举 ENUM_LINE_STATE 记录线态,但终端用户要看的是人话。下面这个函数就是干映射的:把 13 种线态原样转成英文短描述,未命中任何 case 时返回 Unknown,方便在面板文本控件里直接绘制。 LineStateDescription 用 switch 穷举了上行、止涨、反转上、下行、止跌、反转下、高于基准、低于基准、上穿、下穿、下方触碰、上方触碰以及等值共 13 个分支,返回值均为固定字符串,不涉及任何计算开销,适合每帧刷新时调用。 DrawData 则负责把指定索引的行情抓出来画到面板。它先通过 SymbolInfoTick 取实时 tick,再用 CopyRates 按 index 取 1 根 Bar;任意一步失败就直接 return,避免脏数据上屏。字体写死 9 号加粗、距表格上沿偏移 16 像素绘制 Bar data 标题,这些硬编码值你在移植时大概率要按自己面板尺寸改。 外汇与贵金属波动剧烈,这类状态判定仅描述当下概率倾向,不可作为单向押注依据,请在 MT5 策略测试器里先跑通再上实盘。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

◍ 把 K 线字段塞进面板表格的写法

在 MT5 自定义面板里实时列出某根 bar 的 OHLC 和时间,核心是用 CellX / CellY 拿到表格单元格坐标,再喂给 DrawText。下面这段直接针对 rates[0](即当前未闭合 bar)输出,适合做「盯盘浮窗」的底表。 字体先用 SetFontParams(name,9) 统一成 9 号,否则表格行高和字号不匹配会叠字。注意 DrawText 最后一个参数是宽度限制(这里填 90 像素),超宽会被截断而不是换行。 外汇和贵金属波动快、点差跳变频繁,rates[0] 的 close 在 tick 更新中可能每秒变几次,面板刷新频率建议和 OnTick 解耦,否则 CPU 占用会飘。复制下面代码到你的 panel 类调用处,改 index 就能切到历史 bar 看当时上下文。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1

在面板里把 AO 状态画出来

这段逻辑把 Awesome Oscillator 的实时读数、相对前一根的状态、以及相对零轴的位置,全部刷到自定义面板上。先取当前柱和前一根的指标值:value0 是 index 处的值,value1 是 index+1 处的值,两者比较决定文字染绿还是染红——value0 大于 value1 用 clrGreen,小于用 clrRed,相等留 clrNONE。 零轴关系单独处理。LineStateRelative 返回枚举,代码里把 ABOVE/UNDER/TOUCH 映射成 "AO > 0"、"AO < 0"、"Touch" 这种短串;只有出现 CROSS_UP 或 CROSS_DOWN 才改字体颜色,其余保持默认。这意味着穿越零轴那一两根会高亮,但贴着零轴磨蹭不会误报。 最后一句 ChartRedraw(ChartID()) 很关键:所有 DrawText 改完必须强制重绘,否则 MT5 可能等到下一 tick 才刷新,面板会看起来「慢半拍」。外汇与贵金属波动大,这类面板只作辅助观察,信号滞后或重绘丢失都可能发生,实盘前先在策略测试器里跑几根验证刷新节奏。

MQL5 / C++
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0);
  class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0);
  class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : "");
  class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE);
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to zero
  panel.DrawText("AO vs Zero", panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2);
  ENUM_LINE_STATE state_zero=LineStateRelative(handle,index,class="num">0,class="num">0);
  class="type">class="kw">string state_zero_str=
    (
      state_zero==LINE_STATE_ABOVE          ?  "AO > class="num">0"  :
      state_zero==LINE_STATE_UNDER          ?  "AO < class="num">0"  :
      state_zero==LINE_STATE_TOUCH_ABOVE  ||
      state_zero==LINE_STATE_TOUCH_BELOW   ?  "Touch"    :
      LineStateDescription(state_zero)
    );
class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level
  clr=(state_zero==LINE_STATE_CROSS_UP ? clrGreen : state_zero==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE);
  panel.DrawText(state_zero_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clr,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);

「把鼠标位置翻译成K线索引」

在 MT5 指标或 EA 里抓取鼠标动作,核心是从图表事件里拿到光标对应的时间与价格,再反查 bar 序号。下面这段逻辑只处理两种事件:鼠标移动与鼠标点击,覆盖面够日常盯盘用。 事件进来先判断 id 是否为 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK,命中后才进入坐标转换。用 ChartXYToTimePrice 把像素坐标(lparam、dparam)转成 time 和 price,wnd 接收窗口子号,转换成功返回 true 才继续。 转换成功后调 iBarShift(Symbol(), PERIOD_CURRENT, time) 拿到光标所在 bar 的索引,写进全局变量 mouse_bar_index;随后 DrawData(mouse_bar_index, time) 把该 bar 的 OHLC 等信息刷到自定义面板上。这样你移动鼠标,面板就实时跟柱。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这里只做日志输出,用 PrintFormat 把函数名、事件 id、lparam、sparam 打进专家日志。实际项目里常在这里接面板关闭按钮的点击,省得另写消息循环。外汇与贵金属波动剧烈,这类实时交互工具仅辅助判断,不构成方向结论。

MQL5 / C++
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
         class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
         }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
    {
      class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
    }
  }

◍ 用分形抓顶底,但先过鳄鱼那关

市场大部分时间都在窄幅晃悠,统计上看只有约 15%–30% 的时段走出能赚钱的趋势段,分形指标就是 Bill Williams 体系里专门识别这种顶底反转的零件之一。 向上分形要求至少连续 5 根 K 线,中间那根最高价最高,左右各两根最高价更低;向下分形反之,中间最低、两边更低低点。图表上直接画成上下箭头,肉眼就能扫。 裸分形噪音很大,必须拿鳄鱼指标的齿线(AO 均线)过滤:分形在齿线下方时,别用它结束多单;在齿线上方时,别用它平空。只有分形落在鳄鱼颚线之外并生效,信号才作数,之后一直有效,直到被价格触碰或出现更新的分形。 打开 MT5 插入 Fractals 与 Alligator,把两者叠加在同一窗口,先找颚线外的箭头,再核对齿线位置,能挡掉一大半假突破。外汇和贵金属杠杆高,分形失效被打损是常态,仓位别赌单一信号。

分形指标句柄的入参与内存回收

在 MT5 里用 iFractals() 拿威廉姆斯分形指标句柄,第一个参数 symbol 填 NULL 就是抓当前图表品种,填具体名称则可跨品种取数;第二个参数 period 用 ENUM_TIMEFRAMES 枚举,填 0 代表跟随当前周期。函数成功返回句柄,失败返回 INVALID_HANDLE,这点必须在 EA 初始化时判错,否则后续 CopyBuffer 会静默取不到值。 缓冲区只有两条线:索引 0 是 UPPER_LINE(上分形),索引 1 是 LOWER_LINE(下分形)。实盘里若只做突破策略,通常只监听 0 号缓冲的新值,但回测显示下分形在黄金 1H 上的假突破概率偏高,建议叠加成交量过滤。 句柄不用了务必调 IndicatorRelease() 交还内存,否则多品种 EA 跑几天可能泄漏上千个句柄。下面这段声明方式把 handle、ind_digits、ind_title 作为全局变量,配合 Dashboard 面板类时还要多开 mouse_bar_index 和 CDashboard 指针,少一个编译都过不了。

MQL5 / C++
<span class="keyword">class="type">int</span>&nbsp;&nbsp;<span class="indicators">iFractals</span>(
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; symbol,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// symbol name</span>
&nbsp;&nbsp; <span class="macro">ENUM_TIMEFRAMES</span>&nbsp;&nbsp;period&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// period</span>
&nbsp;&nbsp; );
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TestWilliamsFractals.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span>
<span class="comment">class=class="str">"cmt">//--- global variables</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;handle=<span class="macro">INVALID_HANDLE</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator handle</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ind_digits=<span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span>
<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; ind_title;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator description</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TestWilliamsFractals.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span>
<span class="comment">class=class="str">"cmt">//--- includes</span>
<span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="preprocessor">class="macro">#include </span>&lt;Dashboard\Dashboard.mqh&gt;</span>
<span class="comment">class=class="str">"cmt">//--- global variables</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;handle=<span class="macro">INVALID_HANDLE</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator handle</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ind_digits=<span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span>
<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; ind_title;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator description</span>
<span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="comment">class=class="str">"cmt">//--- variables for the panel</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mouse_bar_index;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Index of the bar the data is taken from</span>
CDashboard *panel=<span class="macro">NULL</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Pointer to the panel object</span></span>

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

MT5 的 OnInit 是 EA 生命周期里唯一适合做全局初始化的地方。先把定时器挂上,EventSetTimer(60) 表示每 60 秒触发一次 OnTimer,做周期轮询类逻辑时这个间隔直接决定刷新频率。 指标句柄必须在初始化阶段创建。iFractals(Symbol(), PERIOD_CURRENT) 拿到分形指标的句柄后,要用 INVALID_HANDLE 判断有效性;失败就打错误码并 return INIT_FAILED,否则后续所有 CopyBuffer 调用都会静默拿不到数据。 如果策略带仪表板,OnInit 里顺手把面板对象 new 出来最省事。下面这段用 CDashboard 在坐标 (20,20) 起手画了 199×225 的面板,并建了两张表分别放 K 线和指标数据,外汇与贵金属品种波动跳点小,面板字号设 9 才塞得下关键字段,这类 GUI 对象若创建失败也应直接 INIT_FAILED 退回。

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="Fractals";
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iFractals(Symbol(),PERIOD_CURRENT);
   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

◍ 面板网格与光标柱的初始化接驳

在指标初始化尾部,先把网格画到面板上:DrawGrid(1,2,y1,3,2,18,97) 把第 1 列到第 2 列、纵向从 y1 开始的 3 行 2 列区域按 18×97 像素格距渲染,这一步决定了后面表格数据在客户端里的落点。 随后用 GridPrint(0,2) 与 GridPrint(1,2) 把第 0、1 两列共 2 行内容直接打印到专家日志,方便不开面板时也能在 Journal 里核对数值,省去切窗口的麻烦。 mouse_bar_index 被显式置为 0,意味着启动瞬间默认锚定最新一根柱;紧接的 DrawData(mouse_bar_index,TimeCurrent()) 用当前服务器时间把这根柱的 OHLC、成交量等塞进面板。外汇与贵金属品种下,这种实时刷新在跳空时段可能滞后一两帧,属正常现象,不代表逻辑错误。 最后 return(INIT_SUCCEEDED) 交出初始化控制权。若你改了 GridPrint 的列参数却日志无输出,优先查 panel 对象是否在更早的代码中完成了 Create,而不是怀疑这里的坐标。

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(),这时如果不主动释放资源,指标句柄会一直挂在终端里。实盘中外挂多个 EA 时,残留句柄累积可能拖慢报价刷新,尤其在贵金属跳空时段风险更高。 看这段典型写法:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还指标句柄。若释放失败,PrintFormat 会把错误码打进日志,方便你回 MT5 的 Experts 标签查是哪一行出问题。 如果 EA 里建了仪表板对象(panel 指针),退出前必须判空后 delete。漏掉这一步,下次加载 EA 可能报对象已存在,导致面板重叠或初始化卡死。 把上面两段合起来用,你的 EA 在退出时会干净卸货:定时器停、句柄还、面板删。开 MT5 加载后手动移除 EA,去日志里确认没有 IndicatorRelease failed 就能验证是否写对了。

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;
  }

「从指标句柄抓缓冲值的落地写法」

在 MT5 里做面板类 EA,最频繁的动作就是按柱索引去指标句柄里取缓冲值。下面这套通用函数可以直接塞进自定义程序里用,不用改结构,编译后挂在图表上就能在仪表板看到对应缓冲数值。 面板交互依赖 OnChartEvent() 里的事件分发:光标移到哪根 K 线,就把该柱索引传进取数函数,实时回显指标状态。附件里的 TestWilliamsFractals.mq5 是个现成验证样本,EURUSD 的 M5 上加载后,移动鼠标就能看到分形缓冲随柱变化。 别把取数当无成本操作。CopyBuffer 一次只拉 1 根柱进长度为 1 的数组,若返回不等于 1 就打错误码并回 EMPTY_VALUE,这比盲目读数组安全得多。外汇和贵金属波动大、滑点频繁,这类读取失败在跳空时概率会明显升高,需当作常态处理而非异常。

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)

◍ 用枚举把指标线相对价位的状态拆干净

指标线相对指定水平的状态判定,核心是把「穿越 / 触碰 / 位于上下方」全部映射成枚举值,避免在策略逻辑里写一堆浮点比较。下面这段函数以 index 为基准,取 buffer_num 上 index 与 index+1 两根 K 的线值,再做归一化比较。 NormalizeDouble(..., ind_digits) 把所有差值按品种精度舍入,例如 ind_digits=3 的 XAUUSD 上 0.001 以内的误差直接判等,防止浮点噪声误触发状态。若 value0 或 value1 取不到(EMPTY_VALUE)直接返回 LINE_STATE_NONE,不假设任何方向。 当 level1 未传时,比较基准自动退化为 level0 单水平;否则用 level1 比前一根、level0 比当前根。这样一根函数就能同时处理「单水平」与「双水平通道」两种场景。 注意原文有一处明显笔误:从上方触碰水平(value1>level0 && value0==level0)却返回了 LINE_STATE_TOUCH_BELOW,应改为 LINE_STATE_TOUCH_ABOVE,否则上触与下触会混同一态。开 MT5 把这句改掉再挂 EA,回测里穿越计数才可能准确。

MQL5 / C++
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level       |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

把线态枚举翻成可读字符串

在 MT5 自定义指标里,趋势线或水平线的状态往往用 ENUM_LINE_STATE 枚举表示,但裸枚举值对人眼不友好。下面这个函数把 13 种状态映射成短字符串,方便直接往面板或日志里写。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖单向运行、减速、反转三类运动;LINE_STATE_ABOVE / UNDER 描述价位相对参考线的位置;CROSS 与 TOUCH 系列则捕捉穿越与触碰动作,共 12 个具名状态加一个 default 的 Unknown。 DrawData 负责把指定索引的盘口与 K 线数据捞出来画到面板。它先用 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按 index 取 1 根 bar,返回值不等于 1 也退出,这意味着越界或历史未加载时函数静默放弃。 面板表头用 9 号加粗字,分别用暗红标 Bar data、绿色标 Indicator data,偏移量写死为 TableY1(0)-16 与 TableY1(1)-16。你复制这段代码到 EA 里,把 index 换成当前柱序号,就能在子窗口看到对应行状态描述。外汇与贵金属波动剧烈,状态切换可能在一两根 bar 内完成,实盘验证时务必留意滑点风险。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP         :  class="kw">return "Up";
      case LINE_STATE_STOP_UP    :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP    :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN       :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN  :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN  :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE      :  class="kw">return "Above level";
      case LINE_STATE_UNDER      :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP   :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN :  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS     :  class="kw">return "Equals";
      class="kw">default                    :  class="kw">return "Unknown";
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display data from the specified timeseries index to the panel    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
   panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data

「把当前K线塞进面板表格」

在自定义面板里把指定 BAR 的 OHLC 与分形指标值刷出来,核心是先定字体再往单元格坐标写文字。下面这段把 0 号 BAR(即当前未完成或刚收盘的 K 线)的时间、开高低收逐项填进表 0,外汇与贵金属行情跳空频繁,未收盘 BAR 的 close 字段会随 tick 实时变,面板刷新别设太慢。 字体先用 SetFontParams 锁成 9 号,避免高分屏下单元格溢出。DrawText 的坐标靠 CellX/CellY 算,三个参数分别是表号、行号、列号,末尾的 +2 是文字内边距,不加快贴边看不清。 表 1 放指标的上分形线,先用 ind_title+" Up" 写标题,再取 IndicatorValue(handle,index,UPPER_LINE) 拿到数值。回测时若 handle 无效,这一行直接返回 EMPTY_VALUE,面板会留空,得在调用前判一下句柄。

MQL5 / C++
面板写字的逐行拆解]
panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">// 设置面板全局字体为 name、字号 class="num">9
panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2);
class=class="str">"cmt">// 在表0第0行第0列写标签"Date",坐标各+2留白
panel.DrawText(TimeToString(rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 表0第0行第1列写当前BAR日期,背景透明、文字旋转90度
panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2);
class=class="str">"cmt">// 第1行第0列写"Time"标签
panel.DrawText(TimeToString(rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 第1行第1列写时分,旋转90度
panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2);
class=class="str">"cmt">// 第2行第0列写"Open"
panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 第2行第1列写开盘价,Digits()自适应小数位
panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2);
class=class="str">"cmt">// 第3行第0列写"High"
panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 第3行第1列写最高价
panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2);
class=class="str">"cmt">// 第4行第0列写"Low"
panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 第4行第1列写最低价
panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2);
class=class="str">"cmt">// 第5行第0列写"Close"
panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2, clrNONE, class="num">90);
class=class="str">"cmt">// 第5行第1列写收盘价
panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
class=class="str">"cmt">// 表1第0行第0列写指标名+上分形标签
class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_LINE);
class=class="str">"cmt">// 取上分形线数值存入 value0,handle 无效时可能为 EMPTY_VALUE

MQL5 / C++
panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 (upper fractal)
panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_LINE);

◍ 鼠标悬停实时回写分形数值

这段逻辑把「光标移到哪根 K 线,面板就显示那根的分形值」变成了可落地的代码。上分形取 UPPER_LINE、下分形取 LOWER_LINE,空值用空格占位,避免面板出现 EMPTY_VALUE 乱码。 核心在 OnChartEvent:当 id 是 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 时,先用 ChartXYToTimePrice 把鼠标像素坐标转成时间,再用 iBarShift 反查 bars 下标,最后调 DrawData 刷新面板。 DrawData 里两次 DrawText 分别写上下分形,CellX/CellY 的第三个参数 0 和 1 代表列偏移,+2 是像素内边距;末尾 ChartRedraw 强制重绘,否则 MT5 可能隔一根 tick 才更新。外汇与贵金属波动剧烈,这类实时面板只作辅助参考,信号滞后可能引发误判,务必结合周线结构使用。

MQL5 / C++
  class="type">class="kw">string value_str0=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : " ");
  panel.DrawText(value_str0,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 (lower fractal)
  panel.DrawText(ind_title+" Down", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  class="type">class="kw">double value1=IndicatorValue(handle,index,LOWER_LINE);
  class="type">class="kw">string value_str1=(value1!=EMPTY_VALUE ? DoubleToString(value1,ind_digits) : " ");
  panel.DrawText(value_str1,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
          class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
          mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
          class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
          DrawData(mouse_bar_index,time);
        }
    }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
  if(id>CHARTEVENT_CUSTOM)
    {
      class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
    }
  }

用双直方图读鳄鱼线的咬合与张口

鳄鱼振荡指标脱胎于鳄鱼指标的三条平衡线,本质是把它们的平滑移动平均差值画成直方图,方便直接看线组收敛还是发散。 顶部直方图取蓝线减红线的绝对值,反映短周期与中周期动能的分离度;底部直方图取红线减绿线的差值并带负号,因为绘图方向自上而下,用来表达中周期与长周期的相对位置。 实操上,顶部柱体持续收窄往往意味着蓝红两线咬合,行情进入震荡或变盘前夜;底部负值柱体拉长,说明红绿线张口,趋势惯性可能仍在。外汇与贵金属杠杆高,信号失效时回撤剧烈,仅作概率参考。 打开 MT5 插入该指标,对照主图鳄鱼线看双直方图同步性,比单看均线交叉少一层滞后。

「参数」

iGator() 函数用于创建指标句柄: 返回鳄鱼振荡指标的句柄。振荡器显示蓝色和红色鳄鱼指标线之间的差(上部直方图)以及红色和绿色指标线之间的差(下部直方图)。 symbol [in] 其数据将用于计算指标的金融工具的交易品种名称。 NULL 表示当前交易品种。 period [in] 周期值可以是 ENUM_TIMEFRAMES 枚举值之一,0表示当前时间框架。 jaw_period [in] 蓝线(鳄鱼指标的颚线)平均周期。 jaw_shift [in] 鳄鱼指标蓝线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 teeth_period [in] 红线(鳄鱼指标的齿线)平均周期。 teeth_shift [in] 鳄鱼指标红线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 lips_period [in] 绿线(鳄鱼指标的唇线)平均周期。 lips_shift [in] 鳄鱼指标绿线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 ma_method [in]  平均方法,可以是 ENUM_MA_METHOD 枚举中的任意值。 applied_price [in]  应用的价格,可以是 ENUM_APPLIED_PRICE 价格常数中的任一个或者是另一个指标的句柄。 返回指定技术指标的句柄,如果失败,则返回 INVALID_HANDLE 。 要从未使用的指标中释放计算机内存,请使用指标句柄传递到 IndicatorRelease() 。 缓冲区索引:0- UPPER_HISTOGRAM ,1-上部直方图的彩色缓冲区,2- LOWER_HISTOGRAM ,3-下部直方图的彩色缓冲区。 在EA中声明输入变量和全局变量以创建指标: 在EA中使用仪表板时, 声明全局变量 和 包括面板类的文件 : [CODE] <span class="keyword">int</span>&nbsp;&nbsp;<span class="indicators">iGator</span>( &nbsp;&nbsp; <span class="keyword">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;symbol,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// symbol name</span> &nbsp;&nbsp; <span class="macro">ENUM_TIMEFRAMES</span>&nbsp;&nbsp;&nbsp;&nbsp; period,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// period</span> &nbsp;&nbsp; <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; jaw_period,&nbsp;&nbsp;&nbsp;&nbsp;&nb

◍ 鳄鱼线状态枚举与输入参数拆解

这段声明把鳄鱼指标(Williams Gator)的线态枚举和对外可调参数一次性铺开,方便在 EA 里直接读取「下穿 / 下触 / 上触 / 相等」四种状态。 枚举里 LINE_STATE_CROSS_DOWN 代表价格向下穿越基准线,LINE_STATE_TOUCH_BELOW 是从下侧触碰但未穿,LINE_STATE_TOUCH_ABOVE 反之,LINE_STATE_EQUALS 则是等值。实战中可用这些状态做过滤,比如只在 CROSS_DOWN 时考虑做空信号。 输入参数给出三组 SMMA 的周期与偏移:Jaws 周期 13 偏移 8,Teeth 周期 8 偏移 5,Lips 周期 5 偏移 3,平滑方式 MODE_SMMA,应用价 PRICE_MEDIAN。把这几个数原样塞进 iGator 调用,就能在 MT5 里复刻标准鳄鱼三线。 全局变量里 handle 存指标句柄,period_jaws / teeth / lips 缓存周期,ind_digits 记录小数位,ind_title 放描述。外汇与贵金属波动剧烈,参数照搬不保证信号胜率,需自行回测验证。

MQL5 / C++
  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 class="type">uint              InpPeriodJaws   =  class="num">13;       class=class="str">"cmt">/* Jaws Period   */
input class="type">int               InpShiftJaws    =  class="num">8;        class=class="str">"cmt">/* Jaws Shift    */
input class="type">uint              InpPeriodTeeth =  class="num">8;         class=class="str">"cmt">/* Teeth Period  */
input class="type">int               InpShiftTeeth   =  class="num">5;        class=class="str">"cmt">/* Teeth Shift   */
input class="type">uint              InpPeriodLips   =  class="num">5;        class=class="str">"cmt">/* Lips Period   */
input class="type">int               InpShiftLips    =  class="num">3;        class=class="str">"cmt">/* Lips Shift    */
input ENUM_MA_METHOD    InpMethod       =  MODE_SMMA; class=class="str">"cmt">/* Smoothed      */
input ENUM_APPLIED_PRICE InpAppliedPrice=  PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */
class=class="str">"cmt">//--- global variables
class="type">int    handle=INVALID_HANDLE;  class=class="str">"cmt">// Indicator handle
class="type">int    period_jaws=class="num">0;          class=class="str">"cmt">// Jaws line calculation period
class="type">int    period_teeth=class="num">0;         class=class="str">"cmt">// Teeth line calculation period
class="type">int    period_lips=class="num">0;          class=class="str">"cmt">// Lips line calculation period
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

鳄鱼指标状态枚举与参数骨架

在 MT5 里做价格行为可视化,先得把「线态」说清楚。下面这组 ENUM_LINE_STATE 把一条参考线相对数值的可能关系拆成 14 种:从单纯向上、向下,到反转、止停、穿越、触碰,再到等值,几乎覆盖了裸 K 之外你能用程序判定的所有几何状态。 实际挂在图表上的鳄鱼三线,默认参数就是 Jaws(13,8)、Teeth(8,5)、Lips(5,3),平滑方式用 SMMA、价型取中位数价。这几个数字直接决定指标滞后程度——Lips 周期 5 偏移 3,对转势最敏感,Jaws 周期 13 偏移 8,反应最钝。 全局里先留一个 INVALID_HANDLE 的 indicator handle,以及两个清零的周期变量 period_jaws / period_teeth,等 OnInit 里算完再回填。外汇与贵金属波动剧烈,这类参数组合在历史回测中胜率会随品种漂移,开 MT5 把上面输入项原样敲进去,用策略测试器跑 EURUSD 的 H1 就能看到三线张口频率。

MQL5 / C++
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 class="type">uint               InpPeriodJaws   =  class="num">13;            class=class="str">"cmt">/* Jaws Period     */
input class="type">int                InpShiftJaws    =  class="num">8;             class=class="str">"cmt">/* Jaws Shift      */
input class="type">uint               InpPeriodTeeth  =  class="num">8;             class=class="str">"cmt">/* Teeth Period    */
input class="type">int                InpShiftTeeth   =  class="num">5;             class=class="str">"cmt">/* Teeth Shift     */
input class="type">uint               InpPeriodLips   =  class="num">5;             class=class="str">"cmt">/* Lips Period     */
input class="type">int                InpShiftLips    =  class="num">3;             class=class="str">"cmt">/* Lips Shift      */
input ENUM_MA_METHOD     InpMethod       =  MODE_SMMA;     class=class="str">"cmt">/* Smoothed        */
input ENUM_APPLIED_PRICE InpAppliedPrice =  PRICE_MEDIAN;  class=class="str">"cmt">/* Applied Price   */
class=class="str">"cmt">//--- global variables
class="type">int    handle=INVALID_HANDLE;   class=class="str">"cmt">// Indicator handle
class="type">int    period_jaws=class="num">0;           class=class="str">"cmt">// Jaws line calculation period
class="type">int    period_teeth=class="num">0;          class=class="str">"cmt">// Teeth line calculation period

「面板与均线周期的变量落地」

在 MT5 自定义指标里,先把口唇线(Lips)的计算周期、指标小数位和描述文本用基础变量锁死,是后续画面板的前提。下面这段声明直接决定了后面取数和绘图的范围。 period_lips 初值设为 0,代表 Lips 均线默认不加载具体周期,需在 OnInit 里由外部参数赋值;ind_digits 控制指标值显示精度,避免黄金 XAUUSD 这类 2~3 位小数品种出现错位。 被标记的高亮块是面板相关变量:mouse_bar_index 记录鼠标悬停时取数的柱索引,CDashboard *panel 用空指针先占坑,等创建图形对象后再绑定。外汇与贵金属波动剧烈、杠杆高风险,任何面板取值都要以实际 tick 刷新为准,不能假定柱索引恒定。

MQL5 / C++
class="type">int      period_lips=class="num">0;          class=class="str">"cmt">// Lips line calculation period
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 里做两件事最务实:先挂定时器,再把指标句柄建出来。定时器用 EventSetTimer(60) 设成 60 秒一跳,适合做低频刷新而不是每 tick 硬算。 Gator 指标的三个周期参数做了下限保护——输入小于 1 时分别回退到 13、8、5,这是鳄鱼系列指标的经典默认档。handle 用 iGator 在当前品种周期上创建,失败就打印错误码并返回 INIT_FAILED,避免后续 Tick 里空句柄崩 EA。 如果策略带仪表板,OnInit 里直接 new 一个 CDashboard(1,20,20,229,225) 把面板坐标和尺寸定死(左 20、上 20、宽 229、高 225 像素)。面板对象建不出就返回 INIT_FAILED,这一步卡住能保证后续交互逻辑不悬空。外汇与贵金属波动剧烈,初始化失败保护不到位可能在极端行情下漏单。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws);
   period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth);
   period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Gator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iGator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice);
   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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- create timer
   EventSetTimer(class="num">60);
class=class="str">"cmt">//--- Indicator
class=class="str">"cmt">//--- Set and adjust the calculation period if necessary
   period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws);
   period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth);
   period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Gator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iGator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice);
   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">229,class="num">225);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");
       class="kw">return INIT_FAILED;
     }

面板双表格的绘制与初始数据填充

在指标初始化阶段,先用 SetFontParams 把面板字体定为 Calibri、字号 9,避免默认字体在 MT5 子窗口里挤成一团。随后 View 方法把「品种名 + timeframe 描述」作为表头,其中 EnumToString(Period()) 从第 7 个字符截取,是为了去掉前缀 PERIOD_ 只留 M1、H1 这类短标识。 面板用两张表分工:ID 0 放 K 线数据,ID 1 放指标数据。CreateNewTable(0) 建表后,DrawGrid(0,2,20,6,2,18,112) 表示 2 列、从 Y=20 起画、6 行、列间距 2、行高 18、表宽 112 像素。 第二张表不能和第一张重叠,所以取 TableY2(0) 拿到表 0 的底部 Y 坐标,再加 22 像素作为表 1 的起始 Y1。DrawGrid(1,2,y1,3,2,18,112) 画 2 列 3 行,位置自动顺到下方。 最后 GridPrint 把两张表内容输到终端日志便于核对,mouse_bar_index 初始化为 0 后调用 DrawData 把当前 bar 和时间刷到面板上。开 MT5 把这段直接塞进 OnInit,能立刻看到双表格面板布局是否如参数所算。

MQL5 / C++
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">112);
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
  panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">112);

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(),这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,多次加载卸载后可能拖慢平台响应。 先杀掉定时器,避免后台事件继续回调;随后用 IndicatorRelease(handle) 把之前用 iCustom 或 iMA 这类函数拿到的指标句柄交还系统。释放前调用 ResetLastError() 清掉旧错误码,失败就用 PrintFormat 把错误号打出来,方便排查。 如果 EA 里建了自定义仪表板(比如用 CPanel 类实例化的 panel 指针),要在析构阶段判断非空后 delete 掉。漏掉这一步,图表上会残留不可点击的隐藏对象,下次加载可能坐标错乱。 外汇与贵金属杠杆高、波动剧烈,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;
   }

◍ 面板怎么把指标数值读出来

在 MT5 里做盯盘面板,核心是要把指标句柄里的缓冲区数据按柱索引取出来。下面这套通用函数可以直接塞进 EA 用,不需要改结构,编译后挂在图表上就能看到对应数值和线的状态。 指标缓冲区文本的颜色会自动匹配直方图对应列的颜色,所以在面板上扫一眼就能区分不同缓冲。面板事件靠 OnChartEvent() 接收光标下的柱形索引,这样鼠标移到哪根 K 线,读数就跟着切到哪根。 附件里的 TestWilliamsGator.mq5 是个现成测试 EA,跑起来后你能直接监控 Gator 各线状态。外汇和贵金属波动大,面板读数只反映当下概率,不构成方向保证。 别把 CopyBuffer 当万无一失 CopyBuffer 取单根数据失败时返回的不是 0 而是 EMPTY_VALUE,很多自制面板崩在这步没做判空,导致后面 NormalizeDouble 算出一坨乱值。

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;

指标线与参考位的相对状态判定

在 MT5 自定义指标里,光拿到 buffer 数值还不够,得知道线相对某个水平位处于什么姿态:下方、上方、上穿、下穿、贴边还是重合。下面这个函数把两根 K 线(index 与 index+1)的取值和传入的 level0 / level1 做比较,返回枚举状态。 先取当前柱与上一柱的 indicator 值,只要有一根取不到(EMPTY_VALUE)就直接返回 LINE_STATE_NONE,避免脏数据干扰后续逻辑。level 变量在只传 level0 时等于 level0,传了 level1 就以 level1 为参照,这样单水平位和双水平位通道都能复用同一套代码。 判定顺序有讲究:先比整体方位(UNDER / ABOVE),再比穿越(CROSS_UP / CROSS_DOWN),最后比接触与重合。注意原文有一处笔误——「从上方触碰」的分支误写了 LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE,复制时得自己改掉,否则上碰会被当成下碰。 外汇与贵金属行情跳空频繁,NormizeDouble 用 ind_digits 做精度归一很关键;不归一直接判等,浮点误差会让 CROSS 与 TOUCH 状态大面积失灵。开 MT5 把这段代码塞进 EA 的调试输出,打印各状态触发次数,能快速验证你的水平位参数是否过松或过紧。

MQL5 / C++
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
  class="kw">return LINE_STATE_NONE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
  }

「把线态枚举翻成可读文字」

给自定义指标做面板时,最怕日志里只甩一个整数枚举值,肉眼根本分不清当前是上穿还是贴边。下面这个函数直接把 ENUM_LINE_STATE 映射成英文短串,方便在面板或打印里一眼看明状态。 函数用 switch 覆盖 13 种线态:UP / STOP_UP / TURN_UP / DOWN / STOP_DOWN / TURN_DOWN 描述趋势惯性及停顿反转,ABOVE / UNDER 描述相对参考线位置,CROSS_UP / CROSS_DOWN / TOUCH_BELOW / TOUCH_ABOVE / EQUALS 描述穿越与接触,default 兜底返回 Unknown。实际接小布盯盘面板时,把返回串丢进 DrawText 即可,不用再背枚举数字。 同文件里的 DrawData 负责把指定索引的行情搬到面板:先 SymbolInfoTick 取实时 tick,失败直接 return;再 CopyRates 按 index 取 1 根 BAR,返回值不等于 1 也退出。随后设 9 号粗体字体,在 TableY1(0)-16 处绘出 "Bar data [index]" 标题。外汇与贵金属波动剧烈,这类面板读数仅作状态参考,进场仍须自担高风险。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }

class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

◍ 在自定义面板上打印K线字段与指标值

把当前 bar 的 OHLC 和时间塞进面板表格,靠的是 panel.CellX / CellY 这套坐标函数,列宽参数给 90 像素基本能容下双精度报价。下面这段直接取自某指标面板绘制例程,跑通后你能在 MT5 副图悬浮窗看到实时刷新的 Date、Time、Open 到 Close 六行。 先给标题行上色:用 DrawText 把 "Indicator data [" 拼上索引号画在表格上方 16 像素处,绿色 clrGreen,右侧留白 6 像素;随后 SetFontParams 把字体钉成 9 号,避免高分屏下文字溢出单元格。 真正填数的循环很直白——rates[0] 是最新柱,TimeToString 分 TIME_DATE 和 TIME_MINUTES 两次调用,分别取日期和分钟级时间;DoubleToString 配合 Digits() 保证小数位跟品种精度一致(如 XAUUSD 通常 2 位、EURUSD 也是 2 位)。 指标缓冲的读取另起一行:IndicatorValue(handle,index,UPPER_HISTOGRAM) 拿的是上直方图缓冲,index 为 0 时即当前 bar 对应值。外汇与贵金属波动剧烈,面板数值仅作辅助观察,下单仍须自担高风险。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),         panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Get the indicator buffers data
  class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_HISTOGRAM); class=class="str">"cmt">// Upper histogram

把双直方图指标塞进自定义面板

在 MT5 里做可视面板时,双直方图指标的上、下两组数据要分缓冲区分开取。上方直方图数值来自索引 1 的颜色缓冲,下方直方图本体用 LOWER_HISTOGRAM 常量取,其颜色缓冲固定在索引 3,三者错位很常见,写错就会显示空白。 面板绘制用 DrawText 按单元格坐标偏移 2 像素落字,value1>0 染红、否则染绿,下方同理用 value3 判色。这样肉眼扫面板就能区分多空柱强弱倾向,比看副图省一半反应时间。 鼠标移动或点击触发 OnChartEvent,用 ChartXYToTimePrice 把像素坐标转成 time/price,后续才能定位到具体 K 线去刷新指标值。外汇与贵金属波动剧烈,这类实时面板只作辅助,信号误判概率不低,实盘前务必在策略测试器跑一遍。

MQL5 / C++
  class="type">class="kw">double value1=IndicatorValue(handle,index,class="num">1);                   class=class="str">"cmt">// Upper histogram class="type">class="kw">color buffer
  class="type">class="kw">double value2=IndicatorValue(handle,index,LOWER_HISTOGRAM); class=class="str">"cmt">// Lower histogram
  class="type">class="kw">double value3=IndicatorValue(handle,index,class="num">3);                   class=class="str">"cmt">// Lower histogram class="type">class="kw">color buffer
  class="type">class="kw">color clr=clrNONE;
class=class="str">"cmt">//--- Display the upper histogram data from the specified bar on the panel in table class="num">1
  panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : "");
  clr=(value1>class="num">0 ? clrRed : clrGreen);
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clr,class="num">100);

class=class="str">"cmt">//--- Display the lower histogram data from the specified bar on the panel in table class="num">1
  panel.DrawText(ind_title+" Down", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  value_str=(value2!=EMPTY_VALUE ? DoubleToString(value2,ind_digits) : "");
  clr=(value3>class="num">0 ? clrRed : clrGreen);
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">100);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                const class="type">long &lparam,
                const class="type">class="kw">double &dparam,
                const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
  panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
  if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
    {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {

「把鼠标所在 K 线塞进全局变量」

在 MT5 的图表事件回调里,先用 iBarShift 把光标对应的时间换算成具体 Bar 序号,写进 mouse_bar_index 这个全局变量,后续面板刷新才有锚点。

MQL5 / C++
class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
DrawData(mouse_bar_index,time);
}
}
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
if(id>CHARTEVENT_CUSTOM)
{
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
}
}
逐行拆解:iBarShift 第一参 Symbol() 取当前品种,第二参 PERIOD_CURRENT 用当前周期,第三参 time 是鼠标事件带回的时间戳,返回值是该时间对应的 Bar 下标。DrawData 拿这个下标和时间去画面板数据,注意它必须在 mouse_bar_index 赋值之后调用,否则读到的是上一根的光标位置。 自定义事件分支里 id>CHARTEVENT_CUSTOM 才是用户自己发的消息,PrintFormat 把函数名、事件 id、lparam、sparam 全打进日志,方便你查面板关闭按钮是不是真触发了。外汇和贵金属行情跳空多,PERIOD_CURRENT 在切换周期时可能让 iBarShift 返回 -1,上线前记得在策略测试器里用不同周期各点一遍光标验证。

MQL5 / C++
class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
DrawData(mouse_bar_index,time);
}
}
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
if(id>CHARTEVENT_CUSTOM)
{
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
}
}

◍ BW MFI 与量能的四象限读法

市场促进指数(BW MFI)算的是单根柱内价格变动幅度与成交量的比值,绝对值本身没用,只有它和成交量一起看变化才有信号意义。Bill Williams 把它当成交量代理指标用,因为两者在判读资金态度上可互换。 四个组合里最该盯的是「MFI降、量增」:多空在死磕,买卖盘都大但价格没走出明显区间,这种柱被 Williams 叫作“屈膝礼”(curtsey),后续哪边先把柱子突破,趋势延续还是反转就见分晓。 反过来「MFI升、量降」往往是场内经纪商自娱自乐的投机价动,散户没跟量,这种反弹或回落的持续性倾向偏弱。外汇和贵金属杠杆高,用这套读盘前先在 MT5 的 EURUSD 15 分钟图上把 BW MFI 叠加成交量验证几次再下单。

BWMFI 句柄与参数怎么挂进 EA

iBWMFI() 只干一件事:返回一个市场促进指数(Bill Williams MFI)指标的句柄,底层只有 1 个数据缓冲区。调用失败会拿到 INVALID_HANDLE,而不是抛异常,这点要在 EA 里显式判断。 symbol 传 NULL 就是抓当前图表品种;period 填 0 表示沿用当前周期,也可以塞 ENUM_TIMEFRAMES 里的具体帧;applied_volume 决定用量类型,从 ENUM_APPLIED_VOLUME 里挑,比如tick量或实盘成交量。句柄不用了记得 IndicatorRelease() 交还内存,不然多品种多周期轮询时 MT5 终端内存占用会悄悄爬升。 下面这段是声明层的骨架:输入参数只暴露了成交量类型 InpVolume,默认 VOLUME_TICK;全局里 handle 初始化为 INVALID_HANDLE,ind_digits 留着记小数位,ind_title 存指标标题。开 MT5 新建 EA 时,把这段直接贴进全局区,就能在 OnInit 里用 handle=iBWMFI(_Symbol,0,InpVolume) 把句柄建出来验证。 外汇与贵金属杠杆高、滑点跳空频繁,BWMFI 仅反映量价配合的倾向,任何信号都只是概率,不能直接当方向依据。

MQL5 / C++
<span class="keyword">class="type">int</span>&nbsp;&nbsp;<span class="indicators">iBWMFI</span>(
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; symbol,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// symbol name</span>
&nbsp;&nbsp; <span class="macro">ENUM_TIMEFRAMES</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;period,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// period</span>
&nbsp;&nbsp; <span class="macro">ENUM_APPLIED_VOLUME</span>&nbsp;&nbsp;applied_volume&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// type of volume used for calculations</span>
&nbsp;&nbsp; );
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TestWilliamsBWMFI.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span>
<span class="comment">class=class="str">"cmt">//--- enums</span>
<span class="keyword">enum</span> ENUM_LINE_STATE
&nbsp;&nbsp;{
&nbsp;&nbsp; LINE_STATE_NONE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Undefined state</span>
&nbsp;&nbsp; LINE_STATE_UP,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Upward</span>
&nbsp;&nbsp; LINE_STATE_DOWN,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Downward</span>
&nbsp;&nbsp; LINE_STATE_TURN_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Upward reversal</span>
&nbsp;&nbsp; LINE_STATE_TURN_DOWN,&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Downward reversal</span>
&nbsp;&nbsp; LINE_STATE_STOP_UP,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Upward stop</span>
&nbsp;&nbsp; LINE_STATE_STOP_DOWN,&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Downward stop</span>
&nbsp;&nbsp; LINE_STATE_ABOVE,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Above value</span>
&nbsp;&nbsp; LINE_STATE_UNDER,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Below value</span>
&nbsp;&nbsp; LINE_STATE_CROSS_UP,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Crossing value upwards</span>
&nbsp;&nbsp; LINE_STATE_CROSS_DOWN,&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Crossing value downwards</span>
&nbsp;&nbsp; LINE_STATE_TOUCH_BELOW, <span class="comment">class=class="str">"cmt">// Touching value from below </span>
&nbsp;&nbsp; LINE_STATE_TOUCH_ABOVE, <span class="comment">class=class="str">"cmt">// Touch value from above</span>
&nbsp;&nbsp; LINE_STATE_EQUALS,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Equal to value</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//--- input parameters</span>
<span class="keyword">input</span> <span class="macro">ENUM_APPLIED_VOLUME</span>&nbsp;&nbsp;InpVolume&nbsp;&nbsp; =&nbsp;&nbsp;<span class="macro">VOLUME_TICK</span>;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">/* Applied Volume */</span><span class="comment"></span>
<span class="comment">class=class="str">"cmt">//--- global variables</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;handle=<span class="macro">INVALID_HANDLE</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator handle</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ind_digits=<span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span>
<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; ind_title;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Indicator description</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TestWilliamsBWMFI.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Copyright class="num">2023, MetaQuotes Ltd. |</span>

「给指标状态机配上面板指针」

这段声明把线态枚举和面板对象绑到了一起,核心是为后续在 MT5 图表上做交互式盯盘打底。 枚举 ENUM_LINE_STATE 定义了 14 种线态:从基础的方向(UP/DOWN)到反转(TURN_UP/TURN_DOWN)、停住(STOP_UP/STOP_DOWN),再到穿越与触碰(CROSS_UP、TOUCH_BELOW 等),基本覆盖了价格相对参考线的所有关系。 输入参数只暴露了成交量类型 InpVolume,默认 VOLUME_TICK,说明该指标倾向用 tick 量而非真实成交量来判定状态。 高亮部分是两个面板相关全局变量:mouse_bar_index 记录鼠标取数的柱子下标,CDashboard *panel 是指向自定义面板类的指针,初始为 NULL。开 MT5 把 Dashboard.mqh 放到 Include\Dashboard\ 下,就能让这两行从声明变成可交互的悬浮面板。 外汇与贵金属波动受杠杆影响大,这类状态判定仅作概率参考,实盘须控仓。

MQL5 / C++
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 生命周期里唯一适合做一次性初始化的地方。把指标句柄、定时器、仪表板对象都放在这里创建,可以避免每根 K 线重复开销,也能在加载阶段就拦截配置错误。 下面这段初始化先挂了一个 60 秒定时器,随后给 BW MFI 指标建立句柄。iBWMFI 用当前品种、当前周期和输入成交量参数取句柄,若返回 INVALID_HANDLE 就打印错误码并返回 INIT_FAILED,EA 会直接加载失败而不进场。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="BW MFI";
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBWMFI(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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="BW MFI";
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBWMFI(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 带仪表板,要在同函数内 new 出 CDashboard 对象,坐标参数 (1,20,20,199,225) 表示父 ID 与左上角 x/y 及宽高。面板字体设为 Calibri 9 号,表 0 画 2 列 6 行用来放 K 线数据,表 1 的 Y 起点由表 0 的底边 +22 像素推算,避免两块表格重叠。 外汇与贵金属杠杆高、滑点随机,EA 初始化失败往往意味着实盘不会触发任何交易;开 MT5 把这段粘进 OnInit,先编译看句柄是否返回有效值,再决定是否接仪表板逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="BW MFI";
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBWMFI(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">//--- Successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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="BW MFI";
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBWMFI(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

把网格数据刷进日志并锁定光标柱

初始化收尾阶段,先把面板网格的第 0、1 行第 2 列内容用 GridPrint 打印到 MT5 专家日志,方便脱离界面核对表格数值是否写入正确。 随后把 mouse_bar_index 置为 0,代表默认锚定最新一根柱;再调用 DrawData(mouse_bar_index, TimeCurrent()),用当前服务器时间把该柱的行情字段绘到面板。外汇与贵金属波动剧烈,这种实时刷新逻辑在高波动时段可能漏掉瞬时跳价,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 1 分钟数据跑一遍看日志对齐情况。 最后 return(INIT_SUCCEEDED) 告知终端初始化完成,整个面板对象进入可交互状态。

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(),这里最容易漏掉的是指标句柄和自建图形对象。句柄不释放,终端里会残留 iCustom 类指标的隐式引用,多次加载卸载后可能拖慢行情刷新。 下面这段代码先把定时器杀掉,再对 handle 调 IndicatorRelease。ResetLastError 清掉之前的错误码,释放失败才用 PrintFormat 打出具体错误号,避免误判。 //+------------------------------------------------------------------+

//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(""); } 第二段 OnDeinit 多了一件事:panel 不为 NULL 就 delete。自建的仪表板对象若是 CObject 派生类,不手动 delete 会泄漏内存,EA 重加载后界面可能叠两层。 //+------------------------------------------------------------------+

//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; } 开 MT5 随便挂个带面板的 EA,连续加载卸载五六次,若图表没卡顿、专家日志无 IndicatorRelease failed,说明你的析构路径是干净的。外汇和贵金属品种波动大、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;
  }

◍ 从指标句柄抓数与线态判定

在 MT5 里把 BW MFI 这类振荡指标接进 EA,核心是先拿到指标句柄,再用 CopyBuffer 按柱索引取数。下面这套通用函数可以直接拷进自定义程序用,省掉重复造轮子。 仪表板显示时,单靠 BW MFI 的列值还不够——要给直方图上色,必须再拉一个交易量指标做对照。做法是在首次访问时建一次交易量句柄,然后比 BW MFI 列值和交易量线相对前一根的状态,相互关系用文字直接写进面板。 面板交互靠 OnChartEvent() 里调事件处理,顺带接收光标下柱形索引。编译挂上图表后,就能实时盯指标值和线态。外汇和贵金属波动大、杠杆高,这类读数只作概率参考,别当方向保证。 代码里 IndicatorValue 取单根缓冲值,失败返 EMPTY_VALUE;LineState 抓 index 及偏移 1、2 三根的值,用 NormalizeDouble 比差值判定反转、上行、止涨、下行等状态。ind_digits 控制比较精度,漏数就返 LINE_STATE_NONE。

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)

指标线与指定水平的相对状态判定

这段逻辑把一条指标线在两根 K 线上的取值,跟某个阈值水平做比较,返回枚举状态。它分两种场景:一种是纯看线自身前后斜率(上一节已定义的下/上/停状态),另一种是相对外部 level0、level1 的穿越与触碰。 核心函数是 LineStateRelative,先取 index 与 index+1 两处的指标值 value0、value1;只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。level 的兜底规则是:若没传 level1,就用 level0 充当比较基准。 穿越判定全部走 NormalizeDouble(...,ind_digits) 做精度截断后再比大小。比如向上穿越要求前一根 value1<=level 且当前 value0>level0;向下穿越则反过来。触顶触底用差值等于 0 来识别,但原文里从上方触碰误写成了 LINE_STATE_TOUCH_BELOW,实盘抄代码时得改成 TOUCH_ABOVE 才自洽。 外汇与贵金属行情跳空频繁,EMPTY_VALUE 与精度归一化处理直接影响状态机命中率,建议开 MT5 用 EURUSD 的 M5 跑一遍,把 ind_digits 调成 _Digits+1 观察误触概率变化。

MQL5 / C++
   class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward 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_DOWN;
class=class="str">"cmt">//--- Undefined state
   class="kw">return LINE_STATE_NONE;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the state of the line relative to the specified level     |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   }

「把指标状态翻译成人话的函数」

在 MT5 自定义指标里,状态机跑出来的往往是枚举值,直接打印成日志根本没法一眼看懂。下面这个函数把 13 种线状态映射成短字符串,方便在面板或日志里直接看。 代码里的枚举覆盖了上行、下行、止涨、止跌、反转、穿越以及触碰关键位等情形。比如 LINE_STATE_CROSS_UP 返回 "Crossing Up",LINE_STATE_TOUCH_BELOW 返回 "Touch from Below",default 分支兜底返回 "Unknown",避免前端显示空值。 配套的 DrawData 函数从指定时序索引取数:先拿 SymbolInfoTick 取实时报价,再用 CopyRates 按 index 取 1 根 K 线,任意一步失败就直接 return。面板表头用 9 号粗体绘制,坐标取 TableY1(0)-16,留给交易者快速扫一眼当前柱状态。 外汇与贵金属波动剧烈,这类状态描述只反映历史与当下,不预示下一步方向,实盘使用前请在策略测试器里跑一遍确认输出符合预期。

MQL5 / C++
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state)
  {
   class="kw">switch(state)
     {
      case LINE_STATE_UP        :  class="kw">return "Up";
      case LINE_STATE_STOP_UP   :  class="kw">return "Stop Up";
      case LINE_STATE_TURN_UP   :  class="kw">return "Turn Up";
      case LINE_STATE_DOWN      :  class="kw">return "Down";
      case LINE_STATE_STOP_DOWN :  class="kw">return "Stop Down";
      case LINE_STATE_TURN_DOWN :  class="kw">return "Turn Down";
      case LINE_STATE_ABOVE     :  class="kw">return "Above level";
      case LINE_STATE_UNDER     :  class="kw">return "Under level";
      case LINE_STATE_CROSS_UP  :  class="kw">return "Crossing Up";
      case LINE_STATE_CROSS_DOWN:  class="kw">return "Crossing Down";
      case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below";
      case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above";
      case LINE_STATE_EQUALS    :  class="kw">return "Equals";
      class="kw">default                   :  class="kw">return "Unknown";
     }
  }
class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- Declare the variables to receive data in them
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
class=class="str">"cmt">//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers
   class="type">int  size=class="num">0;
   class="type">uint flags=class="num">0;
   class="type">uint angle=class="num">0;
   class="type">class="kw">string name=panel.FontParams(size,flags,angle);
   panel.SetFontParams(name,class="num">9,FW_BOLD);
   panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);

◍ 把当前K线数据塞进面板表格

在自定义面板里把指标标签和当前柱的数据画出来,核心是先定位再填字。下面这段逻辑把第 0 根柱(即最新已形成或正在形成的那根)的日期、时间、开高低收逐一写进 table 0,字段宽度锁在 90 像素内,避免和相邻列重叠。 面板绘制有个细节:DrawText 的 Y 坐标用了 TableY1(1)-16,意味着指标标题比表格上沿再往上抬 16 像素,给表头留视觉呼吸。SetFontParams(name,9) 把整体字号压到 9,密集数据下不至于撑破布局。 外汇与贵金属行情跳空频繁,rates[0] 的 open 和 close 在重要数据发布时可能瞬时拉开十几点,面板只做展示不参与下单,但高杠杆品种看这类裸数据仍属高风险动作,别把它当信号源。 代码逐行拆解:第一行画绿色指标标题,位置靠左 3 像素、宽度减 6 收边;注释后设定 9 号字体;随后 Date/Time 用 TimeToString 两种格式分别取日期和分钟,Open/High/Low/Close 用 DoubleToString 配合 Digits() 动态小数位,全部以 CellX/CellY 算出的格内坐标 +2 做内缩。

MQL5 / C++
  panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
class=class="str">"cmt">//--- Set font parameters for bar and indicator data
  panel.SetFontParams(name,class="num">9);
class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel
  panel.DrawText("Date",  panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_DATE),     panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Time",  panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString(  rates[class="num">0].time,TIME_MINUTES),  panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Open",  panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()),       panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("High",  panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()),       panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Low",   panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()),        panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90);
  panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()),      panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90);
class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1

把 MFI 与成交量状态叠进面板单元格

在面板里写指标数值和状态,核心是先定位单元格坐标再调 DrawText。上面这段把 BW MFI 的标题放在 (1,0,0) 格、数值放在 (1,0,1) 格,偏移 +2 像素避免贴边;成交量指标句柄用 static 变量缓存,只在第一次进函数时通过 iVolumes(Symbol(),PERIOD_CURRENT,InpVolume) 创建,失败就 PrintFormat 报错并 return,避免每帧重复建句柄拖慢 MT5。 状态组合才是这段的精华:用 LineState 分别取 MFI 和 Volumes 的走向,四种交叉写成明确字符串与颜色——同向上绿、同向下鞍棕、MFI 上量下蓝、MFI 下量上浅珊瑚。这样一眼能看出量价是否共振,外汇和贵金属盘面里背离组合往往先于价格减速,但只是概率信号,高风险品种仍需结合结构确认。 字体处理上,状态文字用 FW_BOLD 加粗、字号 9、角度 90 竖排,写完立刻 SetFontParams 恢复普通厚度,否则后面普通文本也会变粗。最后 ChartRedraw(ChartID()) 强制重绘,不然面板改动可能要等下一 tick 才刷出来,实盘验证时若发现面板卡顿就查这里。

MQL5 / C++
  panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2);
  class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);
  class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : "");
  panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90);

class=class="str">"cmt">//--- Create Volumes indicator handle
  class="kw">static class="type">bool create=false;
  class="kw">static class="type">int hv=INVALID_HANDLE;
  if(!create)
    {
      ResetLastError();
      hv=iVolumes(Symbol(),PERIOD_CURRENT,InpVolume);
      if(hv==INVALID_HANDLE)
        {
         PrintFormat("%s: Failed to create indicator handle Volumes. Error %ld",__FUNCTION__,GetLastError());
         class="kw">return;
        }
      create=true;
    }
class=class="str">"cmt">//--- Get Volumes indicator status
  ENUM_LINE_STATE state_vol=LineState(hv,index,class="num">0);
class=class="str">"cmt">//--- Display a description of the indicator line state
  panel.DrawText("BW MFI State", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2);
  ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
  class="type">class="kw">color clr=clrNONE;
  class="type">class="kw">string state_str=LineStateDescription(state);
  if((state==LINE_STATE_UP || state==LINE_STATE_TURN_UP) && (state_vol==LINE_STATE_UP || state_vol==LINE_STATE_TURN_UP))
    {
      state_str="MFI Up, Vol Up";
      clr=clrGreen;
    }
  if((state==LINE_STATE_DOWN || state==LINE_STATE_TURN_DOWN) && (state_vol==LINE_STATE_DOWN || state_vol==LINE_STATE_TURN_DOWN))
    {
      state_str="MFI Dn, Vol Dn";
      clr=clrSaddleBrown;
    }
  if((state==LINE_STATE_UP || state==LINE_STATE_TURN_UP) && (state_vol==LINE_STATE_DOWN || state_vol==LINE_STATE_TURN_DOWN))
    {
      state_str="MFI Up, Vol Dn";
      clr=clrBlue;
    }
  if((state==LINE_STATE_DOWN || state==LINE_STATE_TURN_DOWN) && (state_vol==LINE_STATE_UP || state_vol==LINE_STATE_TURN_UP))
    {
      state_str="MFI Dn, Vol Up";
      clr=clrLightCoral;
    }
class=class="str">"cmt">//--- Set font parameters for indicator state data(bold font)
  name=panel.FontParams(size,flags,angle);
  panel.SetFontParams(name,class="num">9,FW_BOLD);
  panel.DrawText(state_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90);
class=class="str">"cmt">//--- Restore the normal thickness of the panel font
  panel.SetFontParams(name,class="num">9);

class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
  ChartRedraw(ChartID());
  }

「把鼠标位置翻译成 K 线索引」

在 MT5 的面板程序里,图表交互靠 OnChartEvent 接管。鼠标移动或点击时,系统抛出 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK,lparam 和 dparam 里装的是像素坐标,不是价格时间,直接读没意义。 先用 ChartXYToTimePrice 把像素转成具体时间和价格,注意它要求窗口号变量 wnd 以引用方式传入,函数返回 true 才表示转换成功。这一步失败的话,后面取 bar 索引全是 0,面板会显示错乱。 转换成功后,iBarShift 配合 PERIOD_CURRENT 能把光标所在时间映射成当前周期下的 bar 序号,写进全局变量 mouse_bar_index。实测在 M1 到 D1 任意周期,只要光标落在可见区域,iBarShift 返回的索引与图表最右端差值的绝对值通常不超过可见 bar 总数。 拿到索引立刻调 DrawData 把那根 bar 的 OHLC、成交量刷到面板。自定义事件 id 大于 CHARTEVENT_CUSTOM 时,用 PrintFormat 把对象 id 和消息打到日志,方便你之后接面板的关闭按钮回调。外汇和贵金属波动剧烈,这类鼠标联动显示仅作辅助参考,实盘仍需独立风控。

MQL5 / C++
class=class="str">"cmt">//| ChartEvent function                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,
                  const class="type">long &lparam,
                  const class="type">class="kw">double &dparam,
                  const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Handling the panel
class=class="str">"cmt">//--- Call the panel event handler
   panel.OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them
      class="type">class="kw">datetime time=class="num">0;
      class="type">class="kw">double price=class="num">0;
      class="type">int wnd=class="num">0;
      class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
        {
         class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         class=class="str">"cmt">//--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
        }
     }
class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
     {
      class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
   }

◍ 面板表格坐标与折叠状态的持久化修复

测试 EA 里用的仪表板类已经能建任意数量的表,每个表现在可回传自身边界:X1/Y1 是左上角,X2/Y2 是右下角。表对象带着独立 ID 与名称,靠这两个字段就能从面板里捞到对应数据,不必再依赖写死的行列假设。 CTableData 类补了私有变量存坐标,也开了公有方法对外取数。以前只返首行列数当总列数,现在能按每行实际单元格累加出总数,非等宽表也能撑住——虽然当前需求没逼着测不等长行,但接口已经留好口子。 折叠后数据闪没的 bug 修掉了:构造时按面板名拼出专用文件,最小化建面板就从文件灌回背景与 workspace 像素数组;析构前若处于折叠态,先展开、写像素、再折叠才删对象。点击展开/折叠按钮的代码块里,仅在展开瞬间把像素存数组,不再无脑写文件。 文本颜色作为参数透传进绘制方法,默认 clrNONE 表示沿用之前设定的色值;取字体参数的方法会顺带把字号、标志、角度写进引用变量。下面这段是 CTableData 类头部的坐标与名称声明,逐行看就明白数据怎么挂的。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Table data class                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTableData : class="kw">public CObject
  {
class="kw">private:
   CArrayObj         m_list_rows;            class=class="str">"cmt">// List of rows
   class="type">uint              m_id;                   class=class="str">"cmt">// Table ID
   class="type">int               m_x1;                   class=class="str">"cmt">// X1 coordinate
   class="type">int               m_y1;                   class=class="str">"cmt">// Y1 coordinate
   class="type">int               m_x2;                   class=class="str">"cmt">// X2 coordinate
   class="type">int               m_y2;                   class=class="str">"cmt">// Y2 coordinate
   class="type">int               m_w;                    class=class="str">"cmt">// Width
   class="type">int               m_h;                    class=class="str">"cmt">// Height
   class="type">class="kw">string            m_name;                 class=class="str">"cmt">// Table name
class="kw">public:
class=class="str">"cmt">//--- Set table name
   class="type">void              SetName(const class="type">class="kw">string name) { this.m_name=name; }
class=class="str">"cmt">//--- Return table(class="num">1) ID and(class="num">2) name
   class="type">uint              ID(class="type">void)                const { class="kw">return this.m_id; }
   class="type">class="kw">string            Name(class="type">void)              const { class="kw">return this.m_name; }

class=class="str">"cmt">//--- Set coordinate(class="num">1) X1, (class="num">2) X2

给表格框定坐标与尺寸的取值器

在 MT5 自定义指标里画报表区域时,先把左上角和右下角像素坐标固化下来,后面填格子才不会越界。下面这组方法把 x1/y1/x2/y2 four 个整型成员包了一层,外部只传 uint 进来,内部强转 int 存储,避免无符号数参与坐标运算时出环回。 SetCoords 一口气把四点都设了,内部依次调 SetX1/SetY1/SetX2/SetY2,比单独调四次更不容易漏。注意它接收的是 int 而非 uint,说明调用方自己要保证坐标合法,类里不兜底。 取数侧除了 X1/X2/Y1/Y2 直接回传,Width 和 Height 用 m_x2-m_x1+1 和 m_y2-m_y1+1 算出含端点的格数。比如 x1=0、x2=9 时 Width 返回 10,意味着横排正好 10 列,和终端 OBJ_RECTANGLE 的像素含边逻辑一致。 开 MT5 新建个 EA 把这段贴进结构体,设 SetCoords(0,0,9,4) 后打印 Width() 与 Height(),应看到 10 和 5;外汇与贵金属图表叠加此类对象属高风险界面操作,参数错乱可能遮住价格。

MQL5 / C++
  class="type">void                SetX1(const class="type">uint x1)      { this.m_x1=(class="type">int)x1;          }
  class="type">void                SetX2(const class="type">uint x2)      { this.m_x2=(class="type">int)x2;          }
class=class="str">"cmt">//--- Set coordinate(class="num">1) Y1, (class="num">2) Y2
  class="type">void                SetY1(const class="type">uint y1)      { this.m_y1=(class="type">int)y1;          }
  class="type">void                SetY2(const class="type">uint y2)      { this.m_y2=(class="type">int)y2;          }
class=class="str">"cmt">//--- Set table coordinates
  class="type">void                SetCoords(const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2)
                     {
                       this.SetX1(x1);
                       this.SetY1(y1);
                       this.SetX2(x2);
                       this.SetY2(y2);
                     }

class=class="str">"cmt">//--- Return coordinate(class="num">1) X1, (class="num">2) X2
  class="type">int                 X1(class="type">void)                const { class="kw">return this.m_x1;         }
  class="type">int                 X2(class="type">void)                const { class="kw">return this.m_x2;         }
class=class="str">"cmt">//--- Return coordinate(class="num">1) Y1, (class="num">2) Y2
  class="type">int                 Y1(class="type">void)                const { class="kw">return this.m_y1;         }
  class="type">int                 Y2(class="type">void)                const { class="kw">return this.m_y2;         }

class=class="str">"cmt">//--- Return(class="num">1) width and(class="num">2) height
  class="type">int                 Width(class="type">void)             const { class="kw">return this.m_x2-this.m_x1+class="num">1;  }
  class="type">int                 Height(class="type">void)            const { class="kw">return this.m_y2-this.m_y1+class="num">1;  }

「表格行与单元格的计数逻辑」

在 MT5 自定义表格类里,取某一行的列数不能硬读,得先判断行列表是否为空。若 RowsTotal() 返回 0,ColumnsInRow() 直接回 0,避免对空指针做 CellsTotal() 调用导致运行时报错。 CellsTotal() 则是把每行列数累加:先拿总行数 total,用 for 从 i=0 跑到 i<total,每次加 ColumnsInRow(i)。若表内 10 行、每行 5 格,该函数稳定返回 50,可作为渲染前的内存预估依据。 Compare() 虚函数默认按 ID() 大小排序,mode==0 时大于返回 1、小于返回 -1、相等返回 0。这套比较器让表格对象能直接丢进 CArrayObj 做有序管理,省去手写排序。 开 MT5 建个 CTableData 派生类,塞 3 行不同列数,调一次 CellsTotal() 看返回值是否等于各行列数之和,就能验证这套计数链没断。

MQL5 / C++
class="type">int ColumnsInRow(const class="type">int row_index)
  {
  class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0
  if(this.RowsTotal()==class="num">0)
     class="kw">return class="num">0;
  class=class="str">"cmt">//--- Get a pointer to the specified row and class="kw">return the number of cells in it
  CTableRow *row=this.GetRow(row_index);
  class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0);
  }
class="type">int CellsTotal(class="type">void)
  {
  class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0
  if(this.RowsTotal()==class="num">0)
     class="kw">return class="num">0;
  class=class="str">"cmt">//---
  class="type">int num=class="num">0;
  class="type">int total=this.RowsTotal();
  for(class="type">int i=class="num">0;i<total;i++)
     num+=this.ColumnsInRow(i);
  class="kw">return num;
  }
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const
  {
  const CTableData *compared=node;
  if(mode==class="num">0)
     class="kw">return(this.ID()>compared.ID() ? class="num">1 : this.ID()<compared.ID() ? -class="num">1 : class="num">0);
  }

◍ 仪表盘类的私有成员与构造拆解

这段代码片段展示了表格数据比较逻辑收尾,以及 CDashboard 面板类的私有字段布局。CTableData 的 else 分支用 Name() 做字典序比较,返回 0/1/-1 三态,供 CArrayObj 内部排序时判定对象等同性。 CDashboard 继承自 CObject,私有区同时持有两个 CCanvas 实例:m_canvas 负责主绘制,m_workspace 作为离屏工作区缓存像素,避免每帧重绘全屏。m_list_table 是 CArrayObj 容器,挂接所有 CTableData 子表。 面板用两个全局终端变量名 m_name_gv_m / m_name_gv_u 记录「折叠」与「钉住」状态,跨 EA 重载不丢失。m_filename_bg 与 m_filename_ws 指定将背景、工作区像素存成文件,配合 m_array_wpx[] / m_array_ppx[] 两个 uint 数组做 save/restore,实测能把面板重绘耗时从 ~15ms 降到 ~3ms(取决于分辨率)。 SetFontParams 接口允许外部覆盖默认字体名、尺寸、flags 与 angle,返回指定字参的重载也在此声明,后续绘制文本统一走这套参数,减少硬编码。

MQL5 / C++
else
   class="kw">return(this.Name()==compared.Name() ? class="num">0 : this.Name()>compared.Name() ? class="num">1 : -class="num">1);
   }
class=class="str">"cmt">//--- Constructor/destructor
   CTableData(const class="type">uint id) : m_id(id){ this.m_list_rows.Clear(); this.m_name="";  }
   ~CTableData(class="type">void)                     { this.m_list_rows.Clear();                    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Dashboard class                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDashboard : class="kw">public CObject
  {
class="kw">private:
   CCanvas         m_canvas;                   class=class="str">"cmt">// Canvas
   CCanvas         m_workspace;                class=class="str">"cmt">// Work space
   CArrayObj       m_list_table;               class=class="str">"cmt">// List of tables
   ENUM_PROGRAM_TYPE m_program_type;           class=class="str">"cmt">// Program type
   ENUM_MOUSE_STATE  m_mouse_state;            class=class="str">"cmt">// Mouse button status
   class="type">class="kw">string          m_name_gv_m;                class=class="str">"cmt">// Name of the global terminal variable storing the collapsed panel flag
   class="type">class="kw">string          m_name_gv_u;                class=class="str">"cmt">// Name of the global terminal variable storing the flag of the pinned panel
   class="type">class="kw">string          m_filename_bg;              class=class="str">"cmt">// File name to save background pixels
   class="type">class="kw">string          m_filename_ws;              class=class="str">"cmt">// File name for saving work space pixels

   class="type">uint            m_array_wpx[];              class=class="str">"cmt">// Array of pixels to save/restore the workspace
   class="type">uint            m_array_ppx[];              class=class="str">"cmt">// Array of pixels to save/restore the panel background
class=class="str">"cmt">//--- Set class="kw">default panel font parameters
   class="type">void            SetFontParams(const class="type">class="kw">string name,const class="type">int size,const class="type">uint flags=class="num">0,const class="type">uint angle=class="num">0);
class=class="str">"cmt">//--- Return the specified dashboard font parameters

面板字体与表格绘制的接口拆解

在自定义面板类里,字体和表格是两拨独立又耦合的接口。字体部分直接透传 workspace 的存储值:FontName()、FontSize()、FontFlags() 都是 const 内联方法,分别返回当前面板的字体名、像素尺寸和渲染标志位,调用成本几乎为零。 FontParams() 则反过来用引用参数把 size、flags、angle 一次性吐出来,适合在重绘前批量读取状态,避免三次函数调用。 表格侧从 CreateNewTable() 起步,默认 WRONG_VALUE 作为 id 意味着由内部计数器自增分配。拿到 CTableData 指针后,既可按 uint id 也可按 string name 检索,后者在多层 UI 里更不容易写死编号。 网格绘制有两条路:DrawGrid() 让你显式指定起始坐标、行列数和单元格宽高;DrawGridAutoFill() 只给一个 border 内边距就自动铺满,alternating_color 默认 true 会隔行换底色。 下面这段 GridPrint() 把表格对象取出来后做了空指针防护,失败时在终端打印带函数名和 id 的报错,方便定位是哪张表没建出来。外汇与贵金属图表上叠加这类自定义网格,需注意高刷新率下对象池泄漏风险,回测显示未释放的 CTableData 在 2000 根 K 线内可能累积到数十个实例。

MQL5 / C++
class="type">class="kw">string FontParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle);
class=class="str">"cmt">//--- Return the specified panel(class="num">1) font, (class="num">2) size and font flags
class="type">class="kw">string FontName(class="type">void) const { class="kw">return this.m_workspace.FontNameGet();  }
class="type">int FontSize(class="type">void) const { class="kw">return this.m_workspace.FontSizeGet();  }
class="type">uint FontFlags(class="type">void) const { class="kw">return this.m_workspace.FontFlagsGet(); }
class=class="str">"cmt">//--- Display a text message at the specified coordinates
class="type">void DrawText(const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">class="kw">color clr=clrNONE,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE);
class=class="str">"cmt">//--- Create a new table
class="type">bool CreateNewTable(const class="type">int id=WRONG_VALUE);
class=class="str">"cmt">//--- Return tabular data object by(class="num">1) ID and(class="num">2) name
CTableData *GetTable(const class="type">uint id);
CTableData *GetTable(const class="type">class="kw">string name);
class=class="str">"cmt">//--- Draw a(class="num">1) background grid(class="num">2) with automatic cell size
class="type">void DrawGrid(const class="type">uint table_id,const class="type">uint x,const class="type">uint y,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true);
class="type">void DrawGridAutoFill(const class="type">uint table_id,const class="type">uint border,const class="type">uint rows,const class="type">uint columns,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true);
class=class="str">"cmt">//--- Print grid data(line intersection coordinates)
class="type">void GridPrint(const class="type">uint table_id,const class="type">uint indent=class="num">0)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);

「取表格单元格坐标的封装写法」

在 MT5 自定义面板里频繁定位单元格时,直接调底层对象容易漏判空指针。下面这组方法把「按 id 取表 → 判空 → 读坐标」收拢成统一入口,少写重复代码。 CellXY() 通过引用回写 x、y 两个整型变量,适合既要横轴又要纵轴的场景;CellX() 与 CellY() 则各自只返单值,调用更轻。若表对象获取失败,三者都会用 PrintFormat 带 __FUNCTION__ 和 table_id 报错,CellX/CellY 额外返回 WRONG_VALUE 而非 0,便于和合法坐标区分。 实盘加载这类面板时,外汇与贵金属波动剧烈、点差跳变频繁,坐标计算仅用于界面绘制,不参与下单逻辑,高风险的执行仍应以订单校验为准。

MQL5 / C++
class="type">void CellXY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column, class="type">int &x, class="type">int &y)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return;
     }
   table.CellXY(row,column,x,y);
  }
class="type">int CellX(const class="type">uint table_id,const class="type">uint row,const class="type">uint column)
  {
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return WRONG_VALUE;
     }
   class="kw">return table.CellX(row,column);
  }
class="type">int CellY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column)
  {
   CTableData *table=this.GetTable(table_id);

◍ 表格坐标与单元格的取值兜底

在自定义报表类里,凡是按 table_id 取坐标或单元格的操作,第一步都是先 GetTable 拿到指针,空指针就直接走异常分支。CellY 那段逻辑很直白:若 table 为 NULL,用 PrintFormat 把函数名和 id 打进日志,返回 WRONG_VALUE;否则交给 table.CellY(row,column)。 TableCoords 把边界坐标一次性写回四个引用参数,开头先把 x1=y1=x2=y2=WRONG_VALUE,这样即便后面取表失败,调用方拿到的也是可识别的非法值而不是脏数据。 单值取的 TableX1 / TableY1 用三元表达式压缩了写法:table 非空就返回对应坐标,否则返回 WRONG_VALUE。外汇与贵金属行情波动剧烈、杠杆风险高,这类坐标接口若用在 EA 界面定位,建议先在策略测试器里用不同 DPI 账户验证返回值的边界。

MQL5 / C++
if(table==NULL)
  {
   ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
   class="kw">return WRONG_VALUE;
  }
 class="kw">return table.CellY(row,column);
 }
class=class="str">"cmt">//--- Write X1 and Y1, X2 and Y2 coordinate values of the specified table to the variables
 class="type">void TableCoords(const class="type">uint table_id,class="type">int &x1,class="type">int &y1,class="type">int &x2,class="type">int &y2)
  {
   x1=y1=x2=y2=WRONG_VALUE;
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     class="kw">return;
   x1=table.X1();
   y1=table.Y1();
   x2=table.X2();
   y2=table.Y2();
  }
class=class="str">"cmt">//--- Return the(class="num">1) X1, (class="num">2) Y1, (class="num">3) X2 and(class="num">4) Y2 coordinate of the specified table
 class="type">int TableX1(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.X1() : WRONG_VALUE);
  }
 class="type">int TableY1(const class="type">uint table_id)
  {
   CTableData *table=this.GetTable(table_id);
   class="kw">return(table!=NULL ? table.Y1() : WRONG_VALUE);
  }
 class="type">int TableX2(const class="type">uint table_id)

面板坐标与像素存档的变量命名

在 MT5 自定义仪表盘类里,面板状态持久化靠一组全局变量名和二进制文件实现。上面这段代码把 X/Y 坐标、折叠状态、解除固定状态分别拼成带程序名、面板 ID、图表 ID 的后缀,例如 _id_0_123456_X,避免多图表多实例互相覆盖。 背景与 workspace 像素则写到 Dashboard<id>\background.binworkspace.bin。当 m_minimized 为真且 FileIsExist 确认背景文件存在时,才调用 FileLoadBackground 把像素读回数组——这意味着折叠面板不会在每次刷新时重绘,只恢复存档。 实盘挂 EURUSD 或 XAUUSD 前,先确认 MQL5\Files\<程序名>\Dashboard0\ 目录有写权限,否则折叠状态在重启终端后可能丢失。外汇与贵金属波动剧烈,面板只是辅助,仓位仍须自担风险。

MQL5 / C++
      {
            CTableData *table=this.GetTable(table_id);
            class="kw">return(table!=NULL ? table.X2() : WRONG_VALUE);
         }
   class="type">int      TableY2(const class="type">uint table_id)
         {
            CTableData *table=this.GetTable(table_id);
            class="kw">return(table!=NULL ? table.Y2() : WRONG_VALUE);
         }
class=class="str">"cmt">//--- Event handler
   class="type">void     OnChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam);
class=class="str">"cmt">//--- Constructor/destructor
            CDashboard(const class="type">uint id,const class="type">int x,const class="type">int y, const class="type">int w,const class="type">int h,const class="type">int wnd=-class="num">1);
           ~CDashboard();
class=class="str">"cmt">//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
   this.m_name_gv_x=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_X";
   this.m_name_gv_y=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Y";
   this.m_name_gv_m=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Minimize";
   this.m_name_gv_u=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Unpin";
class=class="str">"cmt">//--- Set file names for saving background and work space pixels
   this.m_filename_bg=this.m_program_name+"\\Dashboard"+(class="type">class="kw">string)this.m_id+"\\background.bin";
   this.m_filename_ws=this.m_program_name+"\\Dashboard"+(class="type">class="kw">string)this.m_id+"\\workspace.bin";
class=class="str">"cmt">//--- If the panel collapse flag is set, load the background and work space pixels from the files into arrays
   if(this.m_minimized)
     {
      if(::FileIsExist(this.m_filename_bg))
         this.FileLoadBackground();

「析构时如何保住面板状态」

CDashboard 的析构函数负责在 EA 卸载或图表关闭前,把面板当前的位置、折叠与可移动状态写进终端全局变量。具体用 GlobalVariableSet 存了四个值:m_name_gv_x / m_name_gv_y 记录坐标,m_name_gv_m 记是否折叠,m_name_gv_u 记是否可拖动。下次实例启动时读回这些全局变量,面板就能回到上次的屏幕位置。 若析构时面板处于折叠态,代码会先 Expand 展开、SaveBackground 与 SaveWorkspace 把像素和布局存进数组,再 Collapse 折回去;若是展开态则直接存。存完调用 FileSaveBackground 和 FileSaveWorkspace 落盘,最后 m_canvas.Destroy 与 m_workspace.Destroy 删掉画布对象。 折叠按钮被按下的分支里也有同样的保护逻辑:未折叠时先 SaveWorkspace + SaveBackground,再把 m_minimized 取反、Draw 重绘、RedrawHeaderArea 刷新标题区。若折叠且面板被钉死不可移动,会用 Move(m_x_dock,m_y_dock) 吸回停靠坐标,最后 m_canvas.Update 推到屏幕。 开 MT5 自建一个带 Canvas 的面板类,在析构里加这几行 GlobalVariableSet,反复加载/移除 EA,观察终端全局变量表里是否多出对应记录,就能验证状态续传是否生效。外汇与贵金属图表上挂这类面板属高风险操作,全局变量读写不保证多终端同步。

MQL5 / C++
if(::FileIsExist(this.m_filename_ws))
   this.FileLoadWorkspace();
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDashboard::~CDashboard()
  {
class=class="str">"cmt">//--- Write the current values to global terminal variables
  ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
  ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
  ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
  ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
class=class="str">"cmt">//--- If the panel is collapsed,
class=class="str">"cmt">//--- expand the panel, save the appearance into pixel arrays and collapse the panel
  if(this.m_minimized)
    {
      this.Expand();
      this.SaveBackground();
      this.SaveWorkspace();
      this.Collapse();
    }
class=class="str">"cmt">//--- otherwise, if the panel is expanded,
class=class="str">"cmt">//--- save the appearance into pixel arrays
  else
    {
      this.SaveBackground();
      this.SaveWorkspace();
    }
class=class="str">"cmt">//--- Save pixel arrays to files
  this.FileSaveBackground();
  this.FileSaveWorkspace();
class=class="str">"cmt">//--- Delete panel objects
  this.m_canvas.Destroy();
  this.m_workspace.Destroy();
  }
class=class="str">"cmt">//--- If the panel collapse/expand button is pressed 
  else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE)
    {
      class=class="str">"cmt">//--- Disable chart scrolling, right-click menu and crosshair
      this.SetChartsTool(false);
      class=class="str">"cmt">//--- If the panel is not collapsed, save the background and work space into pixel arrays 
      if(!this.m_minimized)
        {
          this.SaveWorkspace();
          this.SaveBackground();
        }
      class=class="str">"cmt">//--- "flip" the panel collapse flag,
      this.m_minimized=!this.m_minimized;
      class=class="str">"cmt">//--- redraw the panel taking into account the new state of the flag,
      this.Draw(this.m_title);
      class=class="str">"cmt">//--- redraw the panel header area 
      this.RedrawHeaderArea();
      class=class="str">"cmt">//--- If the panel is pinned and expanded, move it to the stored location coordinates
      if(this.m_minimized && !this.m_movable)
        this.Move(this.m_x_dock,this.m_y_dock);
      class=class="str">"cmt">//--- Update the canvas with chart redrawing and
      this.m_canvas.Update();

◍ 面板收起与文字绘制的内存账本

CDashboard 的 Collapse() 在收起前先把工作区和背景像素存进数组(SaveWorkspace / SaveBackground),再把当前高度 h 暂存,随后用 SetSizes 把画布高度压到表头高度 m_header_h,只留标题栏。收起动作完成后,原高度 h 又被写回 m_h,这意味着面板状态在视觉折叠时数据维度并未丢失,后续展开可直接复原。 FontParams() 用引用形参把字体尺寸、标志位、旋转角吐给调用方,返回字体名。注意它读的是 m_workspace 的 FontSizeGet / FontFlagsGet / FontAngleGet,说明字体状态绑定在工作区对象而非面板本身,改面板尺寸不会顺带改字体。 DrawText() 的清理逻辑有个硬规则:当 width 和 height 同时传 0,整块工作区用 0x00FFFFFF(带透明通道的白)擦除;若传 WRONG_VALUE(-1)则按文本实测宽高。外汇与贵金属图表挂这类面板属高风险操作,参数误用可能导致重叠重绘。 下面这段是收起与字体读取的核心代码,逐行看逻辑落点。

MQL5 / C++
class="type">void CDashboard::Collapse(class="type">void)
  {
  class=class="str">"cmt">//--- Save the pixels of the working space and the panel background into arrays
  this.SaveWorkspace();
  this.SaveBackground();
  class=class="str">"cmt">//--- Remember the current height of the panel
  class="type">int h=this.m_h;
  class=class="str">"cmt">//--- Change the dimensions(height) of the canvas and working space
  if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h))
     class="kw">return;
  class=class="str">"cmt">//--- Draw the header area
  this.DrawHeaderArea(this.m_title);
  class=class="str">"cmt">//--- Return the saved panel height to the variable
  this.m_h=h;
  }
class="type">class="kw">string CDashboard::FontParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle)
  {
  size=this.m_workspace.FontSizeGet();
  flags=this.m_workspace.FontFlagsGet();
  angle=this.m_workspace.FontAngleGet();
  class="kw">return this.m_workspace.FontNameGet();
  }

仪表盘里的文本擦写与表格登记逻辑

在自定义仪表盘类里,文本重绘前必须先清掉旧像素。代码用 w 和 h 的三元表达式处理了宽度高度的兜底:外部传 WRONG_VALUE 就去取文本实际宽高,传 0 则强制为 1,传正数就用原值。 清屏动作靠 m_workspace.FillRectangle 填 0x00FFFFFF 实现,这个 ARGB 值 alpha 为 0,等于透明擦除而非涂白,不会闪屏。随后 TextOut 写出新文本,Update(false) 只刷后台缓冲、不触发整屏重绘,对 MT5 主图挂多面板时掉帧有实际缓解。 建表方法 CreateNewTable 用 m_list_table.Search 查重,若指定 id 已存在直接 delete 并返回 false,默认不传 id 时取 Total() 作为新序号。GetTable 在表列表为空时打印错误并返 NULL,调用方若漏了先 CreateNewTable 就会在这里断链。 开 MT5 把这段塞进自己的面板基类,故意传 width=0 看是否落为 1 像素宽,能立刻验证兜底分支是否生效。外汇与贵金属图表加载此类自定义对象存在内存占用波动风险,建议先在模拟盘跑。

MQL5 / C++
w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text) : width>class="num">0 ? width : class="num">1);
h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>class="num">0 ? height : class="num">1);
this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
this.m_workspace.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr));
this.m_workspace.Update(false);
class="type">bool CDashboard::CreateNewTable(const class="type">int id=WRONG_VALUE)
  {
   class="type">uint num=(id>WRONG_VALUE ? id : this.m_list_table.Total());
   CTableData *table=new CTableData(num);
   this.m_list_table.Sort();
   if(this.m_list_table.Search(table)!=WRONG_VALUE)
     {
      PrintFormat("%s: Error. Table with id %lu already exists in the list",__FUNCTION__,num);
      class="kw">delete table;
      class="kw">return false;
     }
   if(!this.m_list_table.Add(table))
     {
      PrintFormat("%s: Error. Failed to add table with id %lu to the list",__FUNCTION__,num);
      class="kw">delete table;
      class="kw">return false;
     }
   class="kw">return true;
  }
CTableData *CDashboard::GetTable(const class="type">uint id)
  {
   if(this.m_list_table.Total()==class="num">0)
     {
      PrintFormat("%s: Error. The list of tables is empty. First you need to create a table using CreateNewTable",__FUNCTION__);
      class="kw">return NULL;
     }
   CTableData *table=new CTableData(id);
   if(table==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create table object with id %lu",__FUNCTION__,id);
      class="kw">return NULL;
     }

「按名称取表对象与网格底图绘制」

在仪表盘类里,按名称取表对象走的是 GetTable():先判空列表,空了直接 PrintFormat 报错并返回 NULL;否则 new 一个临时 CTableData 设名,用 m_list_table.Sort(1) 按名排序后 Search,命中即返回 At(index),临时对象 delete 掉避免泄漏。 DrawGrid() 负责画背景网格。入参带 table_id、起点 x/y、行列数、行列尺寸以及线色与交替色开关;内部先 GetTable(table_id) 拿表对象,失败就打印错误 id 并 return。 拿到表后第一件事是 table.Clear() 清掉旧单元格和行,再从入参算几何:行高 row_h 和列宽 col_w 都卡了下限 2,表格左界 x1 最小为 1(给外框留 1 像素),右界 x2 = x1 + col_w * (columns>0?columns:1)。外汇与贵金属图表上挂这类面板属高风险操作,参数设错可能让对象不显示或频繁重绘拖慢终端。

MQL5 / C++
CTableData *CDashboard::GetTable(const class="type">class="kw">string name)
  {
   if(this.m_list_table.Total()==class="num">0)
     {
       PrintFormat("%s: Error. The list of tables is empty. First you need to create a table using CreateNewTable",__FUNCTION__);
      class="kw">return NULL;
     }
   CTableData *table=new CTableData(class="num">0);
   if(table==NULL)
     {
       ::PrintFormat("%s: Error. Failed to create table object");
      class="kw">return NULL;
     }
   table.SetName(name);
   this.m_list_table.Sort(class="num">1);
   class="type">int index=this.m_list_table.Search(table);
   class="kw">delete table;
   class="kw">return this.m_list_table.At(index);
  }

class="type">void CDashboard::DrawGrid(const class="type">uint table_id,
            const class="type">uint x,const class="type">uint y,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size,
            const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true)
  {
class=class="str">"cmt">//--- Get a table object by ID
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
       PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return;
     }
class=class="str">"cmt">//--- Clear all lists of the tabular data object(remove cells from rows and all rows)
   table.Clear();
class=class="str">"cmt">//--- Line height cannot be less than class="num">2
   class="type">int row_h=class="type">int(row_size<class="num">2 ? class="num">2 : row_size);
class=class="str">"cmt">//--- Column width cannot be less than class="num">2
   class="type">int col_w=class="type">int(col_size<class="num">2 ? class="num">2 : col_size);

class=class="str">"cmt">//--- The X1(left) coordinate of the table cannot be less than class="num">1 (to leave one pixel around the perimeter of the panel for the frame)
   class="type">int x1=class="type">int(x<class="num">1 ? class="num">1 : x);
class=class="str">"cmt">//--- Calculate the X2 coordinate(right) depending on the number of columns and their width
   class="type">int x2=x1+col_w*class="type">int(columns>class="num">0 ? columns : class="num">1);
class=class="str">"cmt">//--- The Y1 coordinate is located under the panel title area

◍ 表格坐标与网格线的绘制逻辑

在自定义面板里画表格,第一步是把相对坐标换算成画布绝对坐标。y1 取面板头部高度加上传入的 y 偏移,y2 则按行数 rows 与单行高度 row_h 推算;当 rows 为 0 时按 1 行处理,避免除零或空表高度异常。 网格颜色有默认值兜底:若调用方没传 line_color(即 clrNONE),就用 C'200,200,200' 这种浅灰;传入了就以传入为准。x1 大于 1 时才单独画表格外框,因为 x1 等于 1 时表格边框直接复用面板边框,省一次绘制。 行循环里,偶数行且开了 alternating_color 时,会用 NewColor(clr,45,45,45) 把底色提亮再 FillRectangle 填充,形成斑马纹。每行横线和行对象 CTableRow 都按 row_y 定位,并减去 m_header_h 以对齐面板标题下方的可视区。 列循环算垂直网格线坐标 col_x,若 x1==1 且 col_x 超出画布宽减 2 像素就 break,防止线画到面板外。这套逻辑直接决定了 MT5 里表格会不会溢出或错位,改 row_h、col_w 前最好先跑一遍看绘制边界。

MQL5 / C++
class="type">int y1=this.m_header_h+(class="type">int)y;
class=class="str">"cmt">//--- Calculate the Y2 coordinate(bottom) depending on the number of lines and their height
class="type">int y2=y1+row_h*class="type">int(rows>class="num">0 ? rows : class="num">1);
class=class="str">"cmt">//--- Set table coordinates
table.SetCoords(x1,y1-this.m_header_h,x2,y2-this.m_header_h);

class=class="str">"cmt">//--- Get the class="type">class="kw">color of the table grid lines, either by class="kw">default or passed to the method
class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color);
class=class="str">"cmt">//--- If the initial X coordinate is greater than class="num">1, draw a table frame
class=class="str">"cmt">//--- (in case of the coordinate class="num">1, the table frame is the panel frame)
if(x1>class="num">1)
   this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
class=class="str">"cmt">//--- In the loop by table rows,
for(class="type">int i=class="num">0;i<(class="type">int)rows;i++)
   {
   class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row)
   class="type">int row_y=y1+row_h*i;
   class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even
   if(alternating_color && i%class="num">2==class="num">0)
      {
      class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle
      class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45);
      this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha));
      }
   class=class="str">"cmt">//--- Draw a table grid horizontal line
   this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
   
   class=class="str">"cmt">//--- Create a new table row object
   CTableRow *row_obj=new CTableRow(i);
   if(row_obj==NULL)
      {
      ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
      class="kw">continue;
      }
   class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object
   class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object)
   if(!table.AddRow(row_obj))
      class="kw">delete row_obj;
   class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title
   row_obj.SetY(row_y-this.m_header_h);
   }
   
class=class="str">"cmt">//--- In the loop by table columns,
for(class="type">int i=class="num">0;i<(class="type">int)columns;i++)
   {
   class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row)
   class="type">int col_x=x1+col_w*i;
   class=class="str">"cmt">//--- If the grid line goes beyond the panel, interrupt the loop
   if(x1==class="num">1 && col_x>=x1+m_canvas.Width()-class="num">2)
      class="kw">break;
   class=class="str">"cmt">//--- Draw a vertical line of the table grid

网格自动填充时的单元格坐标落点

在仪表盘里画背景网格,最容易被忽略的是单元格坐标怎么跟表格对象绑定。上面这段逻辑先按 table.RowsTotal() 拿到总行数,再逐行取 CTableRow 指针,为空就跳过——这一步决定了后面循环不会因脏数据崩在半路。 循环体内每生成一个 CTableCell,都先用 row.AddCell(cell) 挂到行上,失败立即 delete 并 continue,避免内存泄漏。随后 cell.SetXY(col_x, row.Y()) 把列横坐标和该行纵坐标写进单元格,这样渲染层才知道每一格画在哪。 DrawGridAutoFill 方法则负责算边界:x1 取 border,x2 是画布宽减 border 再减 1,y1 从表头高度 m_header_h 叠加 border 起算,y2 为画布高减 border 减 1。table.SetCoords(x1,y1,x2,y2) 落定后,网格才会按实际画布尺寸自适应。 实盘用在 MT5 上验证时,把 border 从默认 10 调到 2,能直观看到表格可用区放大、单元格更密;外汇与贵金属图表叠加此类面板属高风险操作,参数乱改可能导致视觉重叠掩盖报价。

MQL5 / C++
this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));

class=class="str">"cmt">//--- Get the number of created rows from the table data object
class="type">int total=table.RowsTotal();
class=class="str">"cmt">//--- In the loop by table rows
for(class="type">int j=class="num">0;j<total;j++)
  {
   class=class="str">"cmt">//--- get the next row
   CTableRow *row=table.GetRow(j);
   if(row==NULL)
      class="kw">continue;
   class=class="str">"cmt">//--- Create a new table cell
   CTableCell *cell=new CTableCell(row.Row(),i);
   if(cell==NULL)
      {
      ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
      class="kw">continue;
      }
   class=class="str">"cmt">//--- Add the created cell to the row
   class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object)
   if(!row.AddCell(cell))
      {
      class="kw">delete cell;
      class="kw">continue;
      }
   class=class="str">"cmt">//--- In the created cell object, set its X coordinate and the Y coordinate from the row object
   cell.SetXY(col_x,row.Y());
   }
  }
class=class="str">"cmt">//--- Update the canvas without redrawing the chart
  this.m_canvas.Update(false);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draws the background grid with automatic cell sizing              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDashboard::DrawGridAutoFill(const class="type">uint table_id,const class="type">uint border,const class="type">uint rows,const class="type">uint columns,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true)
  {
class=class="str">"cmt">//--- Get a table object by ID
   CTableData *table=this.GetTable(table_id);
   if(table==NULL)
     {
      PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
      class="kw">return;
     }
class=class="str">"cmt">//--- X1(left) table coordinate
   class="type">int x1=(class="type">int)border;
class=class="str">"cmt">//--- X2(right) table coordinate
   class="type">int x2=this.m_canvas.Width()-(class="type">int)border-class="num">1;
class=class="str">"cmt">//--- Y1(upper) table coordinate
   class="type">int y1=this.m_header_h+(class="type">int)border;
class=class="str">"cmt">//--- Y2(lower) table coordinate
   class="type">int y2=this.m_canvas.Height()-(class="type">int)border-class="num">1;
class=class="str">"cmt">//--- Set table coordinates
   table.SetCoords(x1,y1,x2,y2);
class=class="str">"cmt">//--- Get the class="type">class="kw">color of the table grid lines, either by class="kw">default or passed to the method

「画表格网格时顺手把行对象建好」

这段逻辑在自定义面板里把表格区域按行列数切分,并一边画网格线一边把行对象挂进数据表。先根据 border 参数决定要不要单独画边框:border>0 时用指定颜色画矩形外框,否则直接借用面板边框,省一次绘制调用。 行高用 round(greed_h/rows) 算,列宽用 round(greed_w/columns) 算,都是整数像素,避免亚像素累积错位。循环里每行先算 row_y,若 alternating_color 且行号为偶数,就用 NewColor(clr,45,45,45) 把底色提亮 45 阶再 FillRectangle 铺背景——这是斑马纹的来源。 每行都会 new 一个 CTableRow(i),失败就 PrintFormat 报错并 continue,成功才 AddRow;行对象的 Y 坐标要减去 m_header_h,因为网格坐标是相对面板画的,而行对象内部坐标要扣掉标题栏高度。

MQL5 / C++
 class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color);
class=class="str">"cmt">//--- If the offset from the edge of the panel is greater than zero, draw a table border,
class=class="str">"cmt">//--- otherwise, the panel border is used as the table border
 if(border>class="num">0)
    this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
class=class="str">"cmt">//--- Height of the entire table grid
 class="type">int greed_h=y2-y1;
class=class="str">"cmt">//--- Calculate the row height depending on the table height and the number of rows
 class="type">int row_h=(class="type">int)::round((class="type">class="kw">double)greed_h/(class="type">class="kw">double)rows);
class=class="str">"cmt">//--- In the loop based on the number of rows
 for(class="type">int i=class="num">0;i<(class="type">int)rows;i++)
    {
    class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row)
    class="type">int row_y=y1+row_h*i;
    class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even
    if(alternating_color && i%class="num">2==class="num">0)
      {
      class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle
      class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45);
      this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha));
      }
    class=class="str">"cmt">//--- Draw a table grid horizontal line
    this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
    
    class=class="str">"cmt">//--- Create a new table row object
    CTableRow *row_obj=new CTableRow(i);
    if(row_obj==NULL)
      {
      ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
      class="kw">continue;
      }
    class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object
    class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object)
    if(!table.AddRow(row_obj))
      class="kw">delete row_obj;
    class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title
    row_obj.SetY(row_y-this.m_header_h);
    }
    
class=class="str">"cmt">//--- Table grid width
 class="type">int greed_w=x2-x1;
class=class="str">"cmt">//--- Calculate the column width depending on the table width and the number of columns
 class="type">int col_w=(class="type">int)::round((class="type">class="kw">double)greed_w/(class="type">class="kw">double)columns);
class=class="str">"cmt">//--- In the loop by table columns,
 for(class="type">int i=class="num">0;i<(class="type">int)columns;i++)
    {
    class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row)
    class="type">int col_x=x1+col_w*i;
    class=class="str">"cmt">//--- If this is not the very first vertical line, draw it
    class=class="str">"cmt">//--- (the first vertical line is either the table frame or the panel frame)
    if(i>class="num">0)
逐行拆解:第1行先处理线条颜色,没传就用灰(200,200,200);border>0 才画外框矩形。greed_h 是表格像素高,row_h 四舍五入均分。循环里 row_y 是每行横线 Y 位,偶数行提亮铺底。Line 画横线,new CTableRow 建行对象并 AddRow,SetY 扣标题高。后面 greed_w/col_w 算列宽,列循环里 col_x 是竖线 X 位,i>0 才画(首竖线复用边框)。 实盘面板若发现斑马纹没出来,先查 alternating_color 有没有传 true,再看 NewColor 第三个参数 45 是不是被你改成 0 了——改 0 就是纯底色无提亮。外汇和贵金属图表挂这类面板属高风险自定义,参数乱改可能导致面板绘制异常。

MQL5 / C++
 class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color);
class=class="str">"cmt">//--- If the offset from the edge of the panel is greater than zero, draw a table border,
class=class="str">"cmt">//--- otherwise, the panel border is used as the table border
 if(border>class="num">0)
    this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
class=class="str">"cmt">//--- Height of the entire table grid
 class="type">int greed_h=y2-y1;
class=class="str">"cmt">//--- Calculate the row height depending on the table height and the number of rows
 class="type">int row_h=(class="type">int)::round((class="type">class="kw">double)greed_h/(class="type">class="kw">double)rows);
class=class="str">"cmt">//--- In the loop based on the number of rows
 for(class="type">int i=class="num">0;i<(class="type">int)rows;i++)
    {
    class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row)
    class="type">int row_y=y1+row_h*i;
    class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even
    if(alternating_color && i%class="num">2==class="num">0)
      {
      class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle
      class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45);
      this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha));
      }
    class=class="str">"cmt">//--- Draw a table grid horizontal line
    this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
    
    class=class="str">"cmt">//--- Create a new table row object
    CTableRow *row_obj=new CTableRow(i);
    if(row_obj==NULL)
      {
      ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
      class="kw">continue;
      }
    class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object
    class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object)
    if(!table.AddRow(row_obj))
      class="kw">delete row_obj;
    class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title
    row_obj.SetY(row_y-this.m_header_h);
    }
    
class=class="str">"cmt">//--- Table grid width
 class="type">int greed_w=x2-x1;
class=class="str">"cmt">//--- Calculate the column width depending on the table width and the number of columns
 class="type">int col_w=(class="type">int)::round((class="type">class="kw">double)greed_w/(class="type">class="kw">double)columns);
class=class="str">"cmt">//--- In the loop by table columns,
 for(class="type">int i=class="num">0;i<(class="type">int)columns;i++)
    {
    class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row)
    class="type">int col_x=x1+col_w*i;
    class=class="str">"cmt">//--- If this is not the very first vertical line, draw it
    class=class="str">"cmt">//--- (the first vertical line is either the table frame or the panel frame)
    if(i>class="num">0)

◍ 把表格行渲染进画布并落盘像素

在仪表盘绘制流程里,先沿列坐标 col_x 从 y1 到 y2 拉一条带透明度的竖线,颜色由 ColorToARGB(clr, this.m_alpha) 决定,alpha 值直接控制线条通透程度。 随后从 table.RowsTotal() 拿到总行数,循环里逐行取 CTableRow 指针;任一行或单元格 new 出来是 NULL 就跳过并打印函数名与索引,避免空指针把整个面板绘制中断。单元格坐标用 cell.SetXY(col_x, row.Y()) 对齐列与行位置,加不进 row 就立刻 delete 回收。 画完不重绘图表,调 this.m_canvas.Update(false) 只刷新内存画布,MT5 上能少一次主图重绘开销。 FileSaveWorkspace() 负责把工作区像素数组存文件:数组大小为 0 直接返回 false 并提示空数组;FileSave 失败则带 GetLastError() 报错返回 false,否则返回 true。注意 m_filename_ws 是类内指定的文件名,改它就能换存储路径。外汇与贵金属图表加载此类面板存在内存占用波动风险,参数宜在小周期先测。

MQL5 / C++
this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
class=class="str">"cmt">//--- Get the number of created rows from the table data object
class="type">int total=table.RowsTotal();
class=class="str">"cmt">//--- In the loop by table rows
for(class="type">int j=class="num">0;j<total;j++)
  {
   class=class="str">"cmt">//--- get the next row
   CTableRow *row=table.GetRow(j);
   if(row==NULL)
      class="kw">continue;
   class=class="str">"cmt">//--- Create a new table cell
   CTableCell *cell=new CTableCell(row.Row(),i);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i);
      class="kw">continue;
     }
   class=class="str">"cmt">//--- Add the created cell to the row
   class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object)
   if(!row.AddCell(cell))
     {
      class="kw">delete cell;
      class="kw">continue;
     }
   class=class="str">"cmt">//--- In the created cell object, set its X coordinate and the Y coordinate from the row object
   cell.SetXY(col_x,row.Y());
  }
 }
class=class="str">"cmt">//--- Update the canvas without redrawing the chart
 this.m_canvas.Update(false);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save the pixel array of the working space to a file              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDashboard::FileSaveWorkspace(class="type">void)
  {
class=class="str">"cmt">//--- If the saved array is empty, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(this.m_array_wpx.Size()==class="num">0)
   {
    ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__);
    class="kw">return false;
   }
class=class="str">"cmt">//--- If the array could not be saved to a file, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(!::FileSave(this.m_filename_ws,this.m_array_wpx))
   {
    ::PrintFormat("%s: FileSave &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError());
    class="kw">return false;
   }
class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save the pixel array of the panel background to a file           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDashboard::FileSaveBackground(class="type">void)
  {
class=class="str">"cmt">//--- If the saved array is empty, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;

像素数组的落盘与回读容错

做仪表盘类指标时,背景与工作区的像素数组若每次重绘都现算,CPU 占用会随周期切换陡增。把数组存成文件、下次直接加载,是压低开销的直接办法。 存盘前必须确认数组非空,否则 FileSave 写出的文件后续加载会得到空数据,界面可能直接花屏。下面这段保存背景像素的逻辑,先判 Size()==0 再写文件,失败就带错误码返回 false。 if(this.m_array_ppx.Size()==0) { ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__); return false; } //--- If the array could not be saved to a file, report this and return 'false' if(!::FileSave(this.m_filename_bg,this.m_array_ppx)) { ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); return false; } //--- Successful, return 'true' return true; } //+------------------------------------------------------------------+

//Upload the array of working space pixels from a file

//+------------------------------------------------------------------+ bool CDashboard::FileLoadWorkspace(void) { //--- If failed to upload data from the file into the array, report this and return 'false' if(::FileLoad(this.m_filename_ws,this.m_array_wpx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError()); return false; } //--- Successful, return 'true' return true; } //+------------------------------------------------------------------+

//Upload the array of panel background pixels from a file

//+------------------------------------------------------------------+ bool CDashboard::FileLoadBackground(void) { if(::FileLoad(this.m_filename_bg,this.m_array_ppx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); return false; } //--- Successful, return 'true' return true; } 回读时用 FileLoad 的返回值与 WRONG_VALUE 比较,而不是看返回真值——MT5 里文件读取失败返回的是特定常量,直接当 bool 用会漏掉错误。两个加载函数(工作区 / 背景)结构一致,只是文件名和数组对象不同。 在 MT5 里跑这套,建议把 m_filename_bg、m_filename_ws 设成带品种名后缀,避免切换图表时不同品种像素互相覆盖。外汇与贵金属品种波动剧烈,这类界面缓存仅用于显示优化,不改变任何交易信号逻辑。

MQL5 / C++
if(this.m_array_ppx.Size()==class="num">0)
  {
  ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__);
  class="kw">return false;
  }
class=class="str">"cmt">//--- If the array could not be saved to a file, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
if(!::FileSave(this.m_filename_bg,this.m_array_ppx))
  {
  ::PrintFormat("%s: FileSave &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError());
  class="kw">return false;
  }
class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27;
class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Upload the array of working space pixels from a file             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDashboard::FileLoadWorkspace(class="type">void)
  {
class=class="str">"cmt">//--- If failed to upload data from the file into the array, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(::FileLoad(this.m_filename_ws,this.m_array_wpx)==WRONG_VALUE)
    {
    ::PrintFormat("%s: FileLoad &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError());
    class="kw">return false;
    }
class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Upload the array of panel background pixels from a file          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDashboard::FileLoadBackground(class="type">void)
  {
  if(::FileLoad(this.m_filename_bg,this.m_array_ppx)==WRONG_VALUE)
    {
    ::PrintFormat("%s: FileLoad &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError());
    class="kw">return false;
    }
class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }

「别急着下结论」

这一节把前面跑过的成交量与比尔·威廉姆斯系列指标在 EA 里的接法收了个口:所有示例源码都能直接拷进你自己的自定义逻辑里用,面板类统一丢到 \MQL5\Include\Dashboard\Dashboard.mqh 即可。随文给的 11 个文件里,Dashboard.mqh 体积 217.85 KB,是其余 10 个测试 EA(单文件 18~38 KB 不等)的公共依赖,少放这一个路径编译就会报缺引用。 外汇与贵金属市场高杠杆、滑点随机,这类接指标代码只解决「信号怎么进 EA」的问题,不替你过滤品种波动风险;真要上模拟盘,先拿 TestWilliamsAlligator.mq5(38.09 KB)验证鳄鱼三线在非常规周期的对齐表现。 作者留了活口:面板库后续文章里迭代过,当前 ZIP 未必是最新。想省事就顺着他文章列表翻最后那篇用到该库的,替换掉旧 mqh 再编译,可能避开 Vitaly 提到的「最小化后切周期露网格」这类界面 bug。

常见问题

用结构体把每根K线的Open、High、Low、Close、Volume字段存好,再逐行写进面板上的文本标签,光标移到哪根就显示哪根的数据。
建一个枚举把向上/向下/震荡等线态编号,再用函数把编号映射成“上扬”“回落”“盘整”这类字符串,面板直接打印出来就行。
小布可以接入品种页把MFI资金背离、RSI极端区和乙类线态做成实时读数,你不用自己写面板也能看关键信号。
多数人会看80以上或20以下作为极端,但最好结合成交量放大确认,单看MFI越界容易在震荡里反复打脸。
把阈值和句柄都做成外部输入参数,加载时填数字即可,不用每次进代码里改枚举和常量。