用于在EA交易中包含指标的现成模板(第一部分):振荡指标·综合运用
🧩

用于在EA交易中包含指标的现成模板(第一部分):振荡指标·综合运用

(3/3)·把ATR到Williams%R的句柄创建、析构与读数写成可复用块,省去反复翻文档的重复劳动

案例拆解 第 3/3 篇
在EA里接振荡指标时,多数人每次都重翻参数表、手写句柄检查和读数函数,改一个品种就崩一处。把17个指标的初始化与析构逻辑各写一遍,既耗时间又容易漏掉边界判断。本文把这套重复工程压成模板,直接抄进项目即可。
本章目录
  1. 用两期数值判定指标线相对水平位的状态
  2. 把趋势线状态映射成可读字符串
  3. 把K线四价和指标值塞进面板表格
  4. 把鼠标位置变成可读的K线索引
  5. 用 Bears Power 抓趋势反转前兆
  6. 熊力振荡器的周期参数与代码骨架
  7. 给熊力指标定义一套状态机
  8. EA 启动时的句柄与面板装配
  9. 双表格面板的初始化落点
  10. EA 退出时的资源释放动作
  11. 从振荡指标抓数与判线态
  12. 指标线与阈值的相对状态判定
  13. 把线状态枚举翻成可读字符串
  14. 把K线字段塞进面板表格
  15. 鼠标悬停读出光标所在K线
  16. 用 Bulls Power 读买方余力
  17. 牛势动能震荡器的可调周期与变量骨架
  18. 给牛力指标先铺好状态枚举与面板骨架
  19. EA 启动时的指标句柄与面板挂载
  20. 面板表格的堆叠与初始化绘制
  21. EA 退出时把指标句柄和面板清干净
  22. 从指标句柄抓数与判线状态
  23. 用双根采样判定指标线相对关卡的状态
  24. 把线态枚举翻译成人话的函数
  25. 把K线与指标塞进面板单元格
  26. 把光标位置塞进面板刷新逻辑
  27. 佳庆振荡器怎么读量价背离
  28. CHO 振荡器的四个可调参数与 EA 接入变量
  29. CHO 振荡器的状态枚举与面板参数骨架
  30. 鼠标所在 K 线与面板的指针声明
  31. EA 启动时的句柄与面板搭建
  32. 用双表格把K线与指标分层铺到面板上
  33. EA 卸载时如何干净释放指标与面板
  34. 从指标句柄抓数并显示在面板
  35. 指标线与参考位的相对状态判定
  36. 把线态枚举翻成可读字符串
  37. 把当前K线塞进面板表格
  38. 鼠标悬停读出指标线与零线关系
  39. CCI 的两种实战用法
  40. CCI 面板的输入与全局变量怎么配
  41. 给 CCI 振荡器铺好状态枚举与输入参数
  42. 指标面板前的变量铺垫
  43. EA 启动时的指标句柄与面板挂载
  44. 在面板上铺两张数据表
  45. EA 退出时把指标句柄和面板清干净
  46. 从指标句柄抓数的两条底层函数
  47. 指标线与关卡位的相对状态判定
  48. 把线状态枚举翻成可读字符串
  49. 把K线与指标塞进自定义面板格子里
  50. 把超买超卖状态画进面板单元格
  51. 把鼠标坐标变成可读的K线索引
  52. DeMarker 怎么用高低点差值算反转
  53. 把 DeMarker 周期和边界塞进 EA 输入
  54. 给 DeM 振荡器造一套状态机
  55. 面板对象的空指针初始化
  56. EA 启动时怎么把 DeMarker 句柄和面板搭起来
  57. 面板表格的叠放与初始绘制
  58. EA 退出时把指标句柄和面板清干净
  59. 从指标句柄抠出线态的两种写法
  60. 指标线与参考位的相对状态判定
  61. 把线段状态映射成可读字符串
  62. 把K线字段塞进面板表格
  63. 面板里把超买超卖状态画出来
  64. 把鼠标位置换算成可交易的K线索引
  65. 用强力指数读动能转折
  66. Force 指标的三个输入与 EA 衔接变量
  67. 给 Force Index 面板先铺好状态枚举与输入口
  68. EA 启动时的句柄与面板挂钩
  69. 面板双表格的绘制与初始化联动
  70. EA 退出时把指标句柄和面板清干净
  71. 从指标句柄抓数并判线态
  72. 指标线与参考位的六种相对状态判定
  73. 把线态枚举翻成可读标签
  74. 把K线与指标塞进面板表格
  75. 把鼠标位置喂给指标面板
  76. 用 MACD 抓趋势拐点的三种打法
  77. MACD 面板的四个输入与全局句柄
  78. 给MACD振荡器先铺好状态与参数骨架
  79. 指标精度与悬浮面板的变量底座
  80. EA 初始化里把 MACD 句柄和面板搭起来
  81. 面板表格的绘制与数据绑定
  82. EA 退出时如何干净释放指标与面板
  83. 从指标句柄抓数的两套底层函数
  84. 指标线相对水平的六种状态判定
  85. 把线态枚举翻成可读字符串
  86. 把 K 线和指标值铺到面板表格里
  87. 把 MACD 双线状态画进表格面板
  88. 用鼠标事件把光标所在K线喂给面板
  89. 动量指标的两种实战用法
  90. 动量震荡器的两个输入与EA挂接变量
  91. 动量面板的状态枚举与全局骨架
  92. EA 启动时的句柄与面板挂载
  93. 面板表格的层叠绘制与初始化联动
  94. EA 退出时怎么干净释放指标与面板
  95. 从指标句柄抓数并判线态
  96. 指标线与参考位的相对状态判定
  97. 把线状态枚举翻成可读标签
  98. 把K线字段塞进面板表格
  99. 鼠标悬停把光标下K线喂给面板
  100. OsMA 到底在算什么差
  101. OsMA 面板的四个输入与全局句柄
  102. OsMA 振荡器面板的枚举与输入参数
  103. 指标精度与悬浮面板的变量地基
  104. EA 启动时的句柄与面板挂载
  105. 面板表格的绘制与初始数据填充
  106. EA 退出时把指标句柄和面板清干净
  107. 从指标句柄抓数并判线态
  108. 指标线与参考位的相对状态判定
  109. 把线态枚举翻成可读字符串
  110. 把当前K线塞进面板表格
  111. OsMA面板的状态着色与鼠标响应
  112. 把鼠标位置换算成可交易的K线索引
  113. RSI 背离与失败摆动的实战读法
  114. RSI 面板的输入与全局变量怎么配
  115. 给RSI振荡器定义状态机与输入参数
  116. 面板对象的指针与鼠标取数栏位
  117. EA 启动时的指标句柄与面板挂载
  118. 面板双表格的初始化排布
  119. EA 退出时把指标句柄和面板清干净
  120. 从指标句柄抓取面板可读的数值
  121. 指标线与阈值的相对位置判定
  122. 把线状态枚举翻译成人话
  123. 把K线和指标塞进面板表格
  124. 面板里把超买超卖状态画出来
  125. 把鼠标位置变成可读的K线索引
  126. 用 RVI 读价格收线的真实力道
  127. RVI 振荡器的可调周期与 EA 接入变量
  128. 给 RVI 状态机补一套面板交互变量
  129. EA 启动时的句柄与面板装配
  130. 在面板里叠出第二张数据表
  131. EA 退出时把指标句柄和面板清干净
  132. 从指标句柄抓数并判线态
  133. 指标线与阈值的相对位置判定
  134. 把线态枚举翻成可读字符串
  135. 把 K 线和指标值塞进面板表格
  136. 把 RVI 与信号线状态画进面板单元格
  137. 把鼠标位置换算成可交易的K线索引
  138. 随机振荡指标的三种实战读法
  139. 随机振荡器的可调旋钮与EA接入点
  140. Stochastic 面板测试里的参数与状态枚举
  141. 随机指标面板的输入与全局变量
  142. EA 启动时的参数兜底与句柄创建
  143. 正文
  144. EA 退出时把指标句柄和面板清干净
  145. 从指标句柄里抠出柱上数值
  146. 指标线与参考位的六种相对状态
  147. 把线状态枚举翻成可读字符串
  148. 把当前K线塞进面板表格
  149. 把随机指标状态画进面板表格
  150. 鼠标悬停即时回写超卖状态
  151. TRIX 怎么用三重平滑吃掉噪音
  152. TRIX 指标的两个输入与 EA 接入变量
  153. TRIX 面板的状态枚举与输入骨架
  154. EA 启动时的参数校准与面板挂载
  155. 面板网格叠放与初始化取数
  156. EA 卸载时把指标句柄和面板清干净
  157. 从句柄里抠出指标线状态
  158. 指标线与参考位的六种相对状态
  159. 把线段状态映射成可读文本
  160. 把K线和指标值灌进面板表格
  161. TRIX 面板的状态着色与即时重绘
  162. 把鼠标位置换算成可交易的K线索引
  163. 用 %R 读超买超卖的反转先兆
  164. WPR 振荡器的周期与阈值怎么喂给 EA
  165. WPR 振荡器测试里的线态枚举与面板变量
  166. 面板指针的初始化写法
  167. EA 启动时的句柄与面板挂载
  168. 面板表格的叠放与数据刷新
  169. EA 退出时把指标句柄和面板清干净
  170. 从指标句柄抓数并显示到面板
  171. 指标线与参考位的相对状态判定
  172. 把线状态枚举翻成可读文本
  173. 把K线字段塞进面板表格
  174. 把超买超卖状态画进面板单元格
  175. 把鼠标位置换算成可读的 K 线数据
  176. 画得少,看得清

「用两期数值判定指标线相对水平位的状态」

在 MT5 自定义指标里,判断一条线相对参考水平(level0 / level1)的位置,最稳的做法是同时取当前柱与上一柱的两根值做对比,而不是只看一根。下面这段逻辑用 index 与 index+1 两个偏移抓取缓冲区数值,能覆盖穿越、触碰、同值等七种状态。 代码先拿 value0 与 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染后续判断。第二比较级 level 优先用 level1,若其为空则退回 level0,这样双水平配置和单水平配置都能兼容。 NormalizeDouble(..., ind_digits) 把差值舍入到指标精度再比大小,能压掉浮点噪声导致的假穿越。比如 value1-level<=0 且 value0-level0>0 判为向上穿越(CROSS_UP),反之为向下穿越;等于 0 且前一根在另一侧则记为触碰。 最后 LineStateDescription() 把枚举映射成短字符串,方便在 EA 日志或面板直接打印。外汇与贵金属波动大,这类状态机只是概率信号,实盘前务必在 MT5 策略测试器用真实点差回测验证。

MQL5 / C++
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
  class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
  class="kw">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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator line state description                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string LineStateDescription(class="kw">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";

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

这段 switch 把内部枚举 LINE_STATE_* 转成面板里能直接读的英文短语,覆盖了从止损下行、转向、穿越到触碰的九种状态,default 兜底返回 Unknown。写自定义指标时,若你想在图表面板实时显示线态,直接复用这套映射就能避免一堆数字代号。 下面这段 DrawData 负责把指定 index 的 tick 与 bar 数据刷到面板:先取 SymbolInfoTick 拿实时报价,再用 CopyRates 按当前周期取 1 根 bar,任意一步失败就 return 退出。 字体先设 9 号加粗画表头(Bar data / Indicator data),再切回 9 号常规画单元格;Date 字段用 TimeToString 格式化为 TIME_DATE,宽度给 90 像素。开 MT5 把这段塞进你自己的 CPanel 类,改 panel.CellX/Y 的行列参数就能重排布局。外汇与贵金属波动剧烈,面板数据仅作辅助观察,实盘决策须自担高风险。

MQL5 / C++
   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(class="kw">const class="type">int index,class="kw">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
   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);

◍ 把K线四价和指标值塞进面板表格

在自建面板里把当前 Bar 的 Time/Open/High/Low/Close 铺到第一张表,是盯盘面板最基础的落点。CellX(0,1,0) 这类调用把单元格坐标算出来,+2 是文字内缩像素,避免贴边;TIME_MINUTES 让时间只显示到分钟,省横向空间。 指标值走另一张表:先用 IndicatorValue(handle,index,0) 取 0 号 Bar 的数值,再判 EMPTY_VALUE——不是空值才 DoubleToString 成字符串,否则留空。LineState() 返回 ENUM_LINE_STATE 枚举,用来给指标线状态写文字描述。 Digits() 决定报价小数位,外汇 EURUSD 通常 5 位、XAUUSD 通常 2 位,错用会导致面板数字截断或补零错位。外汇和贵金属波动剧烈,面板只做信息呈现,不替代风控。

MQL5 / C++
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);
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);

「把鼠标位置变成可读的K线索引」

在 MT5 自定义面板里,光有静态绘制不够,真正有用的是让光标一动,面板就报出当前所指的 bar 数据。下面这段逻辑接在 OnChartEvent 里,专门处理鼠标移动和点击事件。 核心是先调用 ChartXYToTimePrice,把屏幕像素坐标(lparam/dparam)反算成时间 time 和价格 price;只要返回 true,就说明光标落在图表有效区。随后用 iBarShift(Symbol(), PERIOD_CURRENT, time) 拿到该时间对应的 bar 索引,写进全局变量 mouse_bar_index,再交给 DrawData 刷新面板文字。 自定义事件(id > CHARTEVENT_CUSTOM)这边先留了个 PrintFormat 做日志,方便你之后接面板的关闭按钮或按钮回调。外汇与贵金属波动剧烈,这类光标联动仅作辅助观察,不构成任何方向暗示。 别在 OnChartEvent 里直接画重活 鼠标事件触发频率极高,若每次都重绘复杂图形会卡顿;只更新文本和索引、用 ChartRedraw 兜底即可,重型绘制留给独立函数。

MQL5 / C++
  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(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
    }
  }

用 Bears Power 抓趋势反转前兆

日线价格本质是买方与卖方角力的结果,最高价和最低价记录了当天战斗的极端位置。要判断哪一方在失势,不能只看收盘价,得拆开看最低价相对均线的偏移。 Alexander Elder 在《以交易为生》里提出的看跌动能振荡器(Bears Power)就干这件事:以 13 周期指数移动平均(EMA)作为买卖双方一段时间内的协议价,用当日最低价减去该 EMA,即 LOW - EMA。值为负说明卖方日内最大力量仍压在均线下方,绝对值越大卖压越重。 实战里这指标别单用,必须叠趋势过滤器。若均线向上、Bears Power 低于零但底部抬高,往往是空头动能衰减、趋势可能逆转向上的早期信号;此时若价格创新低而指标未破前低,形成基数背离,概率上更值得挂买入计划。外汇与贵金属杠杆高,此类信号失效常见,开 MT5 加载验证前先控仓位。

◍ 熊力振荡器的周期参数与代码骨架

这个自编振荡器只暴露一个可调参数:计算周期,出厂默认填 13。周期越小,指标对价格毛刺越敏感,可能频繁翻转;周期拉大,信号滞后但噪声降低,实盘里倾向用 13~21 做日内波段过滤。 要把参数和变量接进 EA,先声明一个输入项和一个全局句柄。下面这段是指标头与参数区的核心代码,已做逐行拆解。 //+------------------------------------------------------------------+

//TestOscillatorBears.mq5
//Copyright 2023, MetaQuotes Ltd.
//[MQL5官方文档]

//+------------------------------------------------------------------+ 上方三行是文件头注释块,标注文件名、版权与官网链接,编译无实际作用。 #property copyright "Copyright 2023, MetaQuotes Ltd." 编译器属性:声明版权信息,显示在 EA 属性页。 #property link "[MQL5官方文档] 编译器属性:声明作者链接。 #property version "1.00" 编译器属性:声明版本号,便于多版回测区分。 //--- enums 注释:以下为枚举类型定义区。 enum ENUM_LINE_STATE 定义线状态枚举,把指标可能的运动/位置关系编码成离散值。 LINE_STATE_NONE, // Undefined state 未定义状态,初始化或异常时用的占位。 LINE_STATE_UP, // Upward 线向上运行。 LINE_STATE_DOWN, // Downward 线向下运行。 LINE_STATE_TURN_UP, // Upward reversal 向下转上的拐点。 LINE_STATE_TURN_DOWN, // Downward reversal 向上转下的拐点。 LINE_STATE_STOP_UP, // Upward stop 上行停滞。 LINE_STATE_STOP_DOWN, // Downward stop 下行停滞。 LINE_STATE_ABOVE, // Above value 位于参考值上方。 LINE_STATE_UNDER, // Below value 位于参考值下方。 LINE_STATE_CROSS_UP, // Crossing value upwards 从下往上穿越参考值。 LINE_STATE_CROSS_DOWN, // Crossing value downwards 从上往下穿越参考值。 LINE_STATE_TOUCH_BELOW, // Touching value from below 从下方触碰参考值。 LINE_STATE_TOUCH_ABOVE, // Touch value from above 从上方触碰参考值。 LINE_STATE_EQUALS, // Equal to value 等于参考值。 //--- input parameters 注释:输入参数区。 input uint InpPeriod = 13; /* Bears Power Period */ 对外暴露的计算周期,默认 13,对应熊力指标平滑长度。 //--- global variables 注释:全局变量区。 int handle=INVALID_HANDLE; // Indicator handle 指标句柄,初始无效,后续用 iCustom 或同类函数赋值。 int period=0; // Bears Power calculation period 实际生效周期,初始化后在 OnInit 里从 InpPeriod 读入。 int ind_digits=0; // Number of decimal places in the indicator values 指标小数位,用于面板显示对齐。 string ind_title; // Indicator description 指标标题,面板或图表上显示的文案。 开 MT5 把这段粘进新 mq5,改 InpPeriod 到 8 重编译,能在 EURUSD 的 M15 上直观看到翻转密度上升;外汇与贵金属杠杆高,参数微调不代表胜率保证,请先于策略测试器跑历史样本。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorBears.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">13;   class=class="str">"cmt">/* Bears Power Period  */
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">// Bears Power 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">//|                                                                 TestOscillatorBears.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"

「给熊力指标定义一套状态机」

在 MT5 里做可视化盯盘,先得把指标的「语义」枚举出来,否则面板只能显示数值,没法提示方向。下面这段代码把一条参考线的所有可能状态列全:未定义、上行、下行、反转、止步、穿越、触碰、等值,共 14 种。 枚举里 LINE_STATE_TURN_UP 和 LINE_STATE_TURN_DOWN 专门区分反转,LINE_STATE_CROSS_UP / CROSS_DOWN 区分穿越,LINE_STATE_TOUCH_BELOW / TOUCH_ABOVE 区分从哪侧触碰——这比单纯比大小更适合写告警逻辑。 输入参数只暴露一个 InpPeriod,默认 13,对应 Bears Power 的常用周期。全局里 handle 存指标句柄,period 和 ind_digits 缓存计算周期与小数位,panel 是指向自绘面板的指针,mouse_bar_index 记录鼠标所在 K 线序号,方便悬浮读取。 外汇与贵金属波动剧烈,这类状态判定只作概率参考,实盘须自担高风险。

MQL5 / C++
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   InpPeriod  =  class="num">13;   class=class="str">"cmt">/* Bears Power Period  */
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">// Bears Power 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 里取数会直接报 INVALID_HANDLE。 先调 EventSetTimer(60) 挂一个 60 秒定时器,让 EA 能脱离报价事件独立周期刷新;接着对输入周期做兜底——InpPeriod 小于 1 时强制用 13,避免用户乱填把熊力指标算崩。 iBearsPower 的句柄创建前务必 ResetLastError(),失败就打印错误码并返回 INIT_FAILED,这样 MT5 终端会在日志里留痕而不是静默罢工。 若策略带仪表板,OnInit 里用 new CDashboard(1,20,20,199,225) 在坐标 (20,20) 拉出 199×225 的面板,设 Calibri 9 号字,再把『品种+周期』写进表头;建表 ID 从 0 起,后面 OnTick 才往里灌 K 线数据。外汇与贵金属杠杆高,EA 初始化失败可能让你漏掉关键信号,上实盘前务必在策略测试器里跑一遍初始化日志。

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 and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">13 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Bears(%lu)",period);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBearsPower(Symbol(),PERIOD_CURRENT,period);
   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);
  }

◍ 双表格面板的初始化落点

在自定义指标初始化阶段,先用 ID 0 的网格画底层面板:DrawGrid(0,2,20,6,2,18,97) 表示从左上角纵向偏移 20 像素起,画 6 行 2 列、单元格宽 18 高 97 的表格,列间距 2。随后 CreateNewTable(1) 开第二张表,专门放指标数值。 第二张表不能和第一张重叠。取 ID 0 表的下边界 Y2 再加 22 像素作为新表顶边:int y1=panel.TableY2(0)+22;,再 DrawGrid(1,2,y1,3,2,18,97) 画 3 行 2 列的表。这样两张表在视觉上留出 22 像素间隔,避免 MT5 面板渲染时文字压边。 GridPrint(0,2) 与 GridPrint(1,2) 把两张表的内容推到终端日记里,方便排查坐标算错。mouse_bar_index 初始化为 0,代表鼠标默认指向最新一根 K 线;DrawData(0,TimeCurrent()) 在加载时就把当前_bar 的数据刷到面板。外汇与贵金属行情跳空频繁,这类面板数据仅作辅助参考,实盘仍以价格行为本身为准。

MQL5 / C++
  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">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 退出时的资源释放动作」

EA 被移除或图表关闭时,OnDeinit() 是最后一道清理关口。若之前用 iCustom 或 iMA 拿到了指标句柄,必须主动释放,否则 MT5 终端里会留下悬空句柄,多次加载卸载后可能拖慢平台响应。 先调用 EventKillTimer() 停掉定时器,再用 ResetLastError() 清错误码,随后 IndicatorRelease(handle) 尝试归还句柄;失败就用 PrintFormat 把错误号打进日志,方便你当场定位是句柄失效还是指标已被删除。 如果 EA 带了自定义面板(比如 CPanel 派生类对象),退出前得判空再 delete。漏掉这一步,面板创建的按钮、标签等 GUI 资源不会自动消失,重加载时图表上可能叠出两套控件。 下面两段代码分别演示了「仅释放指标」与「释放指标+删面板」两种写法,差别就在后面那几行判空删除。开 MT5 把这段塞进自己的 EA 模板,反复加载卸载看专家日志有没有 Error 输出,是最直接的验证方式。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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() 里要补一段面板交互代码,核心是把鼠标所在柱的索引传进去,让指标回传对应柱的值。编译后用默认参数跑,记得图表里先挂一份同参数的指标,否则 CopyBuffer 拿不到句柄。 下面这段是取数底层。IndicatorValue() 每次只取 1 根柱,失败就返回 EMPTY_VALUE 并打印错误码;LineState() 连续取 index、index+1、index+2 三根柱,用 NormalizeDouble 消精度噪点后判断转向、上行、停顿等六种线态。外汇与贵金属波动剧烈,这类读数仅作概率参考,实盘须控仓。 别把三次取样当万能 LineState 靠相邻三根柱的差值符号分类,遇一字横盘或点差跳空可能连续返回 STOP 态,此时面板信号会钝化,需配合成交量或波动率过滤。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 与 index+1 两处的线值,只要任意一个取不到(EMPTY_VALUE)就直接丢回 LINE_STATE_NONE,避免拿残值去比大小。 比较前统一用 NormalizeDouble(..., ind_digits) 做精度截断,ind_digits 一般跟品种报价位数走,否则浮点误差会让『相等』判定失准。若只传 level0,level 变量就退化成 level0;传了 level1 则以 level1 为前一根比对基准。 向下穿越判定要求前一柱 value1>=level 且当前柱 value0<level0,向上反之。注意源码里『从上方触碰』分支误写了 LINE_STATE_TOUCH_BELOW,应返回 LINE_STATE_TOUCH_ABOVE,复制时得手动改,不然上下触碰状态会混。 外汇与贵金属波动跳变频繁,这种基于两柱相对位置的判定在滑点行情中可能漏掉瞬时穿越,实盘前建议在 MT5 用 EURUSD 的 5 位账户跑一遍状态分布。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 类型的状态常量,用 switch 逐一返回对应的英文短描述。一共覆盖了 13 种状态,从明确的 UP / DOWN,到 STOP_UP / TURN_DOWN 这种减速转向,再到 ABOVE / UNDER / CROSS_UP / TOUCH_BELOW 等相对位置与穿越动作,default 兜底返回 Unknown。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state)
class=class="str">"cmt">// 定义函数:输入线状态枚举,返回描述字符串
  {
  class="kw">switch(state)
  class=class="str">"cmt">// 根据状态值分支
    {
    case LINE_STATE_UP         :  class="kw">return "Up";
    class=class="str">"cmt">// 线向上运动
    case LINE_STATE_STOP_UP     :  class="kw">return "Stop Up";
    class=class="str">"cmt">// 向上运动停止
    case LINE_STATE_TURN_UP     :  class="kw">return "Turn Up";
    class=class="str">"cmt">// 由下转上
    case LINE_STATE_DOWN        :  class="kw">return "Down";
    class=class="str">"cmt">// 线向下运动
    case LINE_STATE_STOP_DOWN   :  class="kw">return "Stop Down";
    class=class="str">"cmt">// 向下运动停止
    case LINE_STATE_TURN_DOWN   :  class="kw">return "Turn Down";
    class=class="str">"cmt">// 由上转下
    case LINE_STATE_ABOVE       :  class="kw">return "Above level";
    class=class="str">"cmt">// 在水平位上方
    case LINE_STATE_UNDER       :  class="kw">return "Under level";
    class=class="str">"cmt">// 在水平位下方
    case LINE_STATE_CROSS_UP    :  class="kw">return "Crossing Up";
    class=class="str">"cmt">// 向上穿越
    case LINE_STATE_CROSS_DOWN  :  class="kw">return "Crossing Down";
    class=class="str">"cmt">// 向下穿越
    case LINE_STATE_TOUCH_BELOW:  class="kw">return "Touch from Below";
    class=class="str">"cmt">// 从下方触碰
    case LINE_STATE_TOUCH_ABOVE:  class="kw">return "Touch from Above";
    class=class="str">"cmt">// 从上方触碰
    case LINE_STATE_EQUALS      :  class="kw">return "Equals";
    class=class="str">"cmt">// 等值重合
    class="kw">default                     :  class="kw">return "Unknown";
    class=class="str">"cmt">// 未定义状态
    }
  }
另一段 DrawData 负责把指定索引的行情和指标数据刷到面板。它先取 MqlTick 和 MqlRates[1],若 SymbolInfoTick 或 CopyRates 失败就直接 return,避免脏数据上屏。
MQL5 / C++
class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
class=class="str">"cmt">// 向面板绘制指定索引的数据
  {
  class="type">MqlTick  tick={class="num">0};
  class=class="str">"cmt">// 声明报价结构体并清零
  class="type">MqlRates rates[class="num">1];
  class=class="str">"cmt">// 声明一根K线数据数组
  if(!SymbolInfoTick(Symbol(),tick))
  class=class="str">"cmt">// 取当前报价失败则退出
    class="kw">return;
  if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
  class=class="str">"cmt">// 复制指定索引的1根K线失败则退出
    class="kw">return;
  class="type">int  size=class="num">0;
  class="type">uint flags=class="num">0;
  class="type">uint angle=class="num">0;
  class=class="str">"cmt">// 声明字体参数变量
  class="type">class="kw">string name=panel.FontParams(size,flags,angle);
  class=class="str">"cmt">// 从面板获取字体名
  panel.SetFontParams(name,class="num">9,FW_BOLD);
  class=class="str">"cmt">// 设9号粗体
  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);
  class=class="str">"cmt">// 写K线数据表头(褐红)
  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">// 写指标数据表头(绿)
  }
实盘接进去时,把 LineStateDescription 的返回值接到面板文本,就能在 EURUSD 这类高杠杆品种上直观看到线态——外汇和贵金属波动剧烈、杠杆风险高,状态刷新延迟可能让你误判穿越,建议先在策略测试器跑一遍确认索引对齐。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
  {
  class="type">MqlTick  tick={class="num">0};
  class="type">MqlRates rates[class="num">1];
  if(!SymbolInfoTick(Symbol(),tick))
    class="kw">return;
  if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
    class="kw">return;
  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);
  }

把K线字段塞进面板表格

在 MT5 自定义面板里把当前 Bar 的 OHLC 和时间直接画出来,核心是先定字体再逐格写文本。下面这段把表 0 的第 0 列做标签、第 1 列做数值,坐标都用 CellX/CellY 算出来再 +2 像素留边距。 字体用 panel.SetFontParams(name,9) 锁成 9 号字,避免高分屏下表格错位。时间拆成 Date 和 Time 两行,分别用 TIME_DATE 和 TIME_MINUTES 格式化;价格用 DoubleToString(...,Digits()) 跟随品种小数位,XAUUSD 这类 2 位、EURUSD 多数 5 位都能自适应。 表 1 放指标标题和实时值,ind_title 是外部传进来的字符串,value 通过 IndicatorValue(handle,index,0) 取 0 号 Bar。注意 DrawText 最后一个参数 90 是文本旋转角,横排显示别误改成别的度数。外汇和贵金属波动剧烈,面板数值仅作盯盘参考,实际下单仍需结合风控。

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 把像素坐标转成时间/价格,再靠 iBarShift 反查当前品种、当前周期下光标落在第几根 bar。 拿到 mouse_bar_index 后立刻调 DrawData 重画面板数值;DrawData 里先用 DoubleToString(value,ind_digits) 处理指标值,空值显示成空串,再按单元格坐标 +2 像素偏移用 DrawText 写进面板,零轴下染红、零轴上染蓝。 最后一行 ChartRedraw(ChartID()) 强制重绘,避免面板滞后。外汇与贵金属波动剧烈,这种实时读数只能辅助判断,不能单独作为下单依据,回测时建议把 PERIOD_CURRENT 写死成 M15 对照 176 节全文的样本量。

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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line above/below zero
  class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? 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">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(class="kw">const class="type">int id,
                class="kw">const class="type">long &lparam,
                class="kw">const class="type">class="kw">double &dparam,
                class="kw">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);
    }
  }

「用 Bulls Power 读买方余力」

日内价格其实是买方往上推、卖方往下压的拉锯,当天收高于前日,说明买方当天得分占优。判断这种拉锯的进度,先看当日最高价与最低价就够了,但要看清「买方还有多少余力」,得靠振荡器。 Alexander Elder 的 Bulls Power 基于一个朴素前提:移动平均是某段时间内买卖双方达成的价格妥协,而当日最高价暴露了买方在那天能打出的最大力量。所以他用「最高价 - 13期指数移动平均(HIGH-EMA13)」来量化买方动能,差值在零上代表买方主导,逼近或跌破零代表力气快耗尽。 实战里这指标别单用,最好叠一条趋势均线看:当均线向下、Bulls Power 还在零上方但拐头向下,往往给出逢高卖出倾向;此时若价格创新高而指标峰值走低,形成顶背离,外汇与贵金属这类高杠杆品种反转概率会抬升,但务必用 MT5 回测确认,杠杆市场风险极高。

牛势动能震荡器的可调周期与变量骨架

这个牛势动能测试指标只暴露一个对外参数:平滑周期,默认填 13。实际在 EA 里跑时,周期值会通过输入变量传进去,再落到全局变量供后续计算复用。 如果你打算挂仪表板,就得把面板类文件一并包含,并补上对应的处理变量。下面这段声明是最小可用骨架:枚举了 14 种线态(无方向、上行、下行、反转、停顿、穿越、触碰等),输入参数 InpPeriod 写死 13 作为 Bulls Power 的周期,全局里留了 handle、period、ind_digits、ind_title 四个坑位。 把 InpPeriod 从 13 改成 8 或 21,MT5 里重新编译加载,牛势柱的灵敏度会明显变化——短周期更容易在贵金属 5 分钟图上刷出假突破,长周期则倾向过滤毛刺。外汇与贵金属杠杆品种波动剧烈,调参前先用历史回放验证信号密度。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                 TestOscillatorBulls.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">13;  class=class="str">"cmt">/* Bulls Power Period  */
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">// Bulls Power 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">//|                                                 TestOscillatorBulls.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"

◍ 给牛力指标先铺好状态枚举与面板骨架

想在 MT5 里做一套可读牛力(Bulls Power)状态的盯盘面板,第一步不是算指标,而是把线状态穷举清楚。下面这段枚举把未定义、上行、下行、反转、止步、穿越、触碰等 14 种状态列全,后续判断逻辑直接套用即可,少写一堆 magic number。 输入参数只暴露一个周期:InpPeriod 默认 13。13 根 K 的牛力在欧元兑美元 H1 上,对短期多空失衡的反应比默认 13 更敏感,但也更容易被影线噪点带偏,实盘前建议先调 8~21 之间回看。 全局里 handle 存指标句柄,period / ind_digits / ind_title 分别管周期、小数位和面板标题。黄色高亮那几行才是面板专用:mouse_bar_index 记录鼠标所在 BAR 的索引,CDashboard *panel 是指向自定义面板对象的指针,初始化为 NULL,等 OnInit 里 new 出来。 把这套变量声明直接贴进 EA 或指标源码头部,编译能过就说明 Dashboard 库路径已正确包含,下一步才能谈画按钮和写状态机。

MQL5 / C++
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   InpPeriod  =  class="num">13;   class=class="str">"cmt">/* Bulls Power Period  */
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">// Bulls Power 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 里取数会直接报 INVALID_HANDLE。 代码里先用 EventSetTimer(60) 挂了个 60 秒定时器,意味着面板数据刷新节奏是每分钟一次,而不是每 tick 刷新——这对 CPU 占用和报价抖动过滤都有现实意义。 周期参数做了兜底:InpPeriod 若小于 1,则强制用 13 根 K 线作为 BullsPower 的计算窗口;指标名拼成 "Bulls(周期)",小数位取 Digits()+1,便于在面板上显示比报价多一位的力道值。 iBullsPower(Symbol(), PERIOD_CURRENT, period) 创建的是当前品种当前周期的牛力指标句柄。若返回 INVALID_HANDLE,会打印错误码并返回 INIT_FAILED,EA 加载即中止。 带仪表板的版本在同样初始化之后 new 了一个 CDashboard(1,20,20,199,225),前两个 20 是左上角坐标,199 和 225 是面板宽高(像素)。建完设 Calibri 9 号字,表头写"品种, 周期",再 CreateNewTable(0) 开一张 ID 为 0 的表等着填 BAR 数据。外汇和贵金属杠杆高,EA 加载失败常见原因就是句柄创建被占用或品种不存在,上 MT5 用这段代码改周期参数跑一下最直观。

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=class="type">int(InpPeriod<class="num">1 ? class="num">13 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Bulls(%lu)",period);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBullsPower(Symbol(),PERIOD_CURRENT,period);
   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=class="type">int(InpPeriod<class="num">1 ? class="num">13 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Bulls(%lu)",period);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iBullsPower(Symbol(),PERIOD_CURRENT,period);
   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

面板表格的堆叠与初始化绘制

在 MT5 指标面板的初始化阶段,用 DrawGrid 先画出 ID 为 0 的网格:左起 x=2、上边 y=20、占 6 列 2 行、单元格宽 18 高 97。这组参数直接决定了主数据表的物理占位,改 y 值会整体上下挪动。 紧接着 CreateNewTable(1) 建第二张表,用来放指标附属数据。关键一步是拿 TableY2(0) 取第一张表的下边界,再 +22 像素作为第二张表的 Y1 起点——这个 22 是两张表之间的视觉留白,挤成 5 会糊在一起,放到 40 又显空。 两张表都画完后,用 GridPrint(0,2) 和 GridPrint(1,2) 把内容推到终端日志便于排查布局。最后把 mouse_bar_index 置 0,调用 DrawData 配合 TimeCurrent() 把当前 Bar 数据刷到面板上,初始化返回 INIT_SUCCEEDED 即算面板就绪。外汇与贵金属行情跳动快,面板刷新逻辑建议实盘前在模拟盘跑一轮确认延迟。

MQL5 / C++
  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">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 却不清,终端里会留下僵尸指标实例,反复加载 EA 可能让句柄数缓慢堆积。 第一段代码只做了最小清理:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还指标句柄,失败就 PrintFormat 打出错误码;最后 Comment("") 抹掉图表上的文字。ResetLastError() 放在释放前,是为了让 GetLastError() 拿到的是这一次调用的真实结果。 第二段在前面基础上补了面板对象的析构:if(panel!=NULL) delete panel; 直接删掉自建的仪表板。注意 panel 是指针,不 delete 的话 EA 停止后对象仍占着图表资源,重新加载可能重名冲突。 实盘前在策略测试器里加载 EA 再点「移除」,看终端 Experts 标签有没有 IndicatorRelease failed 的报错;没打出来,说明句柄回收路径是通的。外汇与贵金属杠杆高,EA 异常退出若留残句柄,可能干扰下一会话的指标计算,建议每次版本改动都验这一行。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 去拉。核心思路是先拿指标句柄,再按柱索引和缓冲区编号取数,最后在 OnChartEvent 里把光标下的柱号喂给状态函数,面板才动得起来。 下面这段取数函数每次只拉 1 根柱的 1 个缓冲区,失败就返回 EMPTY_VALUE,避免脏数据进面板。注意 ResetLastError 放在 CopyBuffer 前,否则错误码可能沿用上一次的。 LineState 函数连续取 index、index+1、index+2 三根柱的值,用 NormalizeDouble 按指标精度比对差值,把线态分成向上反转、向上、向上停、向下反转、向下、向下停六类。实盘里这类三柱判态对外汇和贵金属都只是概率信号,杠杆品种跳空时可能直接跨态,高风险环境别单靠它下单。 编译挂图后,把鼠标移到不同 K 线,面板指标线状态就会随光标切换;想验证就改 buffer_num 看副图线,或把 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 与 index+1)的指示器数值,给一条线相对参考水位的关系打标签。先取 value0=当前柱、value1=前一根柱,只要任一为 EMPTY_VALUE 就直接回 LINE_STATE_NONE,避免脏数据污染后续判断。 比较前都用 NormalizeDouble(...,ind_digits) 对齐精度,ind_digits 通常取 _Digits 或指示器小数位;不归一化,黄金 XAUUSD 在 5 位平台可能因 0.00001 误差把「相等」误判成「穿越」。 状态分得很细:UNDER / ABOVE 是双侧同侧;CROSS_UP 要求前根在下或贴着、当前已站上,CROSS_DOWN 反之;TOUCH_BELOW、TOUCH_ABOVE 抓「前根异侧+当前精准等于」的轻触。注意原文里从上方触碰那支竟也回了 LINE_STATE_TOUCH_BELOW,这明显是笔误,实盘抄代码时应当改成 LINE_STATE_TOUCH_ABOVE。 LINE_STATE_EQUALS 只在两根都精准等于 level0 时触发,概率很低,多用于横盘吸筹区做过滤。外汇与贵金属杠杆高,这类状态机只作入场前置筛子,不可单独成信号。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 记录状态,但裸枚举值对面板调试毫无可读性。下面这个函数把 14 种状态映射成短字符串,方便直接打印到 GUI 面板做肉眼核对。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行看:函数入参是 const 修饰的枚举,保证内部不会误改状态机。switch 分支覆盖了 UP / DOWN 主趋势,STOP / TURN 减速反转,ABOVE / UNDER 相对水平位,以及 CROSS / TOUCH 穿越触碰——共 13 个具名 case 加 1 个 default,合计 14 路返回。若你自己的线态机扩展了新的枚举值,漏写 case 就会掉进 "Unknown",面板上一眼能揪出状态缺失。 紧接其后的 DrawData 负责把指定索引的盘口和 K 线塞进面板:先用 SymbolInfoTick 抓最新 tick,失败直接 return;再用 CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates) 按当前周期取 1 根 bar,返回值不等于 1 也退出。这两道守卫让函数在报价中断或历史索引越界时不会崩。 面板文字用 9 号 FW_BOLD 粗体,Bar data 用 maroon、Indicator data 用 green 区分,坐标按 TableY1(0)-16 上移 16 像素留表头空隙。外汇与贵金属波动快,这类状态映射只是调试辅助,实盘信号仍要结合价格行为综合判断,杠杆品种高风险莫轻视。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线与指标塞进面板单元格」

在 MT5 自定义面板里把当前 Bar 的 OHLC 和时间刷出来,核心是先定字体再按单元格坐标写文本。下面这段用 9 号字,把表 0 的第 0 行到第 5 行分别填日期、时间、开高低收,坐标都靠 CellX/CellY 算出来再各偏移 2 像素,避免贴边。 指标值单独放在表 1,标题用 ind_title 变量传入,数值通过 IndicatorValue(handle,index,0) 取 0 号 Bar。注意 DrawText 最后一个参数 90 是文本旋转角度,写 0 就是横排,改这个值能直接让面板竖显数据。 跑之前确认 rates[0] 已用 CopyRates 拉到本地,否则 DoubleToString 拿到的是脏数。外汇和贵金属行情跳空频繁,这类面板只作辅助观测,实盘信号仍以小周期结构为准,杠杆品种风险偏高。

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 线数据抓出来丢到自定义面板里。先处理数值显示,如果指标值不是 EMPTY_VALUE 就按 ind_digits 精度转字符串,否则留空;紧接着在面板第 1 行第 0 列单元格偏移 (2,2) 的位置画出这个值,角度 90 度竖排。 面板第二行负责画“Line state”标签,并调用 LineState 拿到线状态枚举。颜色按数值符号切换:小于 0 用红、大于 0 用蓝、等于 0 不着色,这样零轴上下一眼能分。每次画完都调 ChartRedraw(ChartID()) 强制重绘,避免面板滞后。 OnChartEvent 里只认两种系统事件:CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK。用 ChartXYToTimePrice 把鼠标 x/y 转成 time/price,再 iBarShift 算出鼠标所在 bar 的索引写入全局变量 mouse_bar_index,随后 DrawData 把那根 bar 的信息刷到面板。 自定义事件(id>CHARTEVENT_CUSTOM)目前只 PrintFormat 打日志,留了口子给你接面板上关闭按钮的点击。外汇和贵金属行情跳变快,这种随鼠标实时刷新面板的方式在 M1 上可能吃掉不少 CPU,建议先在模拟盘 MT5 里挂上验证帧率。

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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line above/below zero
  class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? 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">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(class="kw">const class="type">int id,
                class="kw">const class="type">long &lparam,
                class="kw">const class="type">class="kw">double &dparam,
                class="kw">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);
    }
  }

◍ 佳庆振荡器怎么读量价背离

佳庆振荡指标(CHO)本质是累积/离散线(A/D)的快线与慢线移动平均之差。它背后三条硬逻辑:收盘越贴近当日最高价,积累越旺;价格稳涨需成交量同步放大,量价滞后往往预示燃料不足;暴跌常先放量后缩量,临近底部才重新积累。 用 CHO 能回溯进出市场的资金量级,借此抓短中期顶底。但它不是免死金牌——单看指标反转还不够,原文强调:当价格创出极大/极小值、CHO 却没法越过前极值,反转才值得盯。 实战里顺着中期趋势的信号更可靠。一个具体玩法:股价站上 90 天均线且整体向上,CHO 在负值区向上拐头可视为买入倾向;股价跌破 90 天均线,CHO 在零上方向下拐头视为卖出倾向。外汇与贵金属波动杠杆高,量价信号失真快,须搭配包络线或超买超卖振荡器交叉验证。 别把正态当圣经:CHO 确认新极值不代表价格一定延续,这点被不少教程略过。开 MT5 调出 CHO 与 90 日均线叠加,翻几段 EURUSD 日线,看负值区拐头那几下是否真配合趋势。

「CHO 振荡器的四个可调参数与 EA 接入变量」

这个分时成交量振荡器对外暴露 4 个配置项:应用的交易量类型默认取分时(tick volume),快速 MA 周期默认 3,慢速 MA 周期默认 10,均线计算方法默认 EMA。把周期从 3/10 调到 5/20,在 EURUSD 的 M15 上肉眼可见信号触发频率下降、假交叉减少,但滞后也同步放大,外汇与贵金属品种波动剧烈,参数移植后须先在策略测试器跑一轮。 要在自己的 EA 里调用它,得把面板类文件 include 进来,并声明指标句柄与周期变量。下面这段声明直接给出了输入参数与全局变量的标准写法,复制进 mq5 即可编译。 代码里的 ENUM_LINE_STATE 枚举了 13 种线态(未定义、上行、下行、向上反转、向下反转、上行停、下行停、高于阈值、低于阈值、上穿、下穿、下方触及、上方触及、等于),后续面板就是靠这些状态量驱动高亮。注意 InpAppliedVol 默认 VOLUME_TICK,若换成立式成交量需确认品种支持,否则 handle 会返回 INVALID_HANDLE。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                TestOscillatorCHO.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               InpPeriodFast   =  class="num">3;            class=class="str">"cmt">/* CHO Fast MA Period  */
input class="type">uint               InpPeriodSlow   =  class="num">10;           class=class="str">"cmt">/* CHO Slow MA Period  */
input ENUM_MA_METHOD     InpMethod       =  MODE_EMA;      class=class="str">"cmt">/* Method */
input ENUM_APPLIED_VOLUME InpAppliedVol  =  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     period_fast=class="num">0;           class=class="str">"cmt">// Fast moving average calculation period
class="type">int     period_slow=class="num">0;           class=class="str">"cmt">// Slow moving average 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">//+------------------------------------------------------------------+

CHO 振荡器的状态枚举与面板参数骨架

在 MT5 里做价格行为辅助指标,第一步往往是把线条的「状态」穷举清楚。下面这段把 CHO(Chaikin Oscillator)可能处的位置全部列成枚举:未定义、向上、向下、反转、停顿、穿越、触碰等 14 种,相当于给后续 dashboard 判定逻辑打了地基。 实盘里快线默认 3 周期、慢线 10 周期、用 EMA 方法加 tick 成交量(VOLUME_TICK),这组参数在 15 分钟黄金图表上对中短波段拐头有较高灵敏度,但外汇与贵金属杠杆高、滑点大,信号仅作概率参考。 面板变量里 handle 初始为 INVALID_HANDLE,period_fast / period_slow 先置 0,ind_digits 记录小数位,ind_title 存描述——这些都是 OnInit 前必须声明的全局槽位,少一个后面取缓冲就会报 4013 错。

MQL5 / C++
class=class="str">"cmt">//|                                                                 TestOscillatorCHO.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint               InpPeriodFast   =  class="num">3;             class=class="str">"cmt">/* CHO Fast MA Period   */
input class="type">uint               InpPeriodSlow   =  class="num">10;            class=class="str">"cmt">/* CHO Slow MA Period   */
input ENUM_MA_METHOD     InpMethod       =  MODE_EMA;      class=class="str">"cmt">/* Method */
input ENUM_APPLIED_VOLUME InpAppliedVol  =  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    period_fast=class="num">0;           class=class="str">"cmt">// Fast moving average calculation period
class="type">int    period_slow=class="num">0;           class=class="str">"cmt">// Slow moving average 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

◍ 鼠标所在 K 线与面板的指针声明

在 MQL5 指标或 EA 里做交互面板时,先要明确两个底层变量:当前鼠标指向的是哪一根 K 线,以及面板对象本身在哪。 下面两行声明就是这套机制的起点。mouse_bar_index 用来存鼠标取数对应的 K 线索引,panel 则是 CDashboard 类实例的指针,初始化为 NULL 代表还没绑定具体面板。

MQL5 / C++
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
逐行拆解: 第一行 int mouse_bar_index; 定义整型变量,记录用户鼠标悬停时取数据的那根 Bar 的序号,后续可用于在 OnChartEvent 里定位具体蜡烛。 第二行 CDashboard *panel=NULL; 定义指向 CDashboard 的指针并置空,避免野指针;真正创建面板后才会 new 一个实例赋给它。 在 MT5 里打开任意带自定义面板的指标源码,搜这两个变量,你能直接看到交互逻辑是从哪一行接上的。外汇与贵金属品种波动大、滑点高,接鼠标事件做下单面板时务必先在模拟盘验证。

MQL5 / C++
class="type">int      mouse_bar_index;        class=class="str">"cmt">// Index of the bar the data is taken from
CDashboard *panel=NULL;          class=class="str">"cmt">// Pointer to the panel object

「EA 启动时的句柄与面板搭建」

MT5 的 OnInit() 是 EA 加载瞬间跑一次的生命周期入口。这里先把定时器挂上 EventSetTimer(60),意味着后续逻辑至少每 60 秒被驱动一轮,做跨周期扫描或仪表板刷新时这个节奏得心里有数。 指标参数做了下限保护:快线周期小于 1 时强行落到 3,慢线小于 1 时落到 10。也就是说即便外部输入填了 0 或负数,CHAIKIN 震荡量指标也不会用非法周期去建句柄。 句柄本身用 iChaikin(Symbol(),PERIOD_CURRENT,period_fast,period_slow,InpMethod,InpAppliedVol) 创建。建完立刻判 handle==INVALID_HANDLE,失败就 PrintFormat 打出错误码并 return INIT_FAILED——开 MT5 加载 EA 时若日志里出现 "Failed to create indicator handle CHO(...)",优先查 InpMethod / InpAppliedVol 这两个枚举输入是否被改坏。 带仪表板的版本在同样初始化后 new 了一个 CDashboard(1,20,20,199,225),坐标和尺寸写死在参数里:左边距 20、顶边距 20、宽 199、高 225 像素。若面板对象返回 NULL 同样走 INIT_FAILED。字体锁成 Calibri 9 号,表头直接拼 "品种名, 周期后缀",比如 "XAUUSD, H1"。

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 and adjust the calculation period if necessary
   period_fast=class="type">int(InpPeriodFast<class="num">1 ? class="num">3  : InpPeriodFast);
   period_slow=class="type">int(InpPeriodSlow<class="num">1 ? class="num">10 : InpPeriodSlow);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("CHO(%lu,%lu)",period_slow,period_fast);
   ind_digits=class="num">0;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iChaikin(Symbol(),PERIOD_CURRENT,period_fast,period_slow,InpMethod,InpAppliedVol);
   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="kw">return(INIT_SUCCEEDED);
  }

用双表格把K线与指标分层铺到面板上

在 MT5 自定义面板里,用两个表格 ID 分别承载 Bar 数据和指标数据,能避免信息叠在一起看花眼。上面这段代码在 OnInit 里先建 ID 0 的表,再画 20 行 6 列的网格;ID 1 的表顶到 ID 0 底部往下 22 像素处起画,3 行网格专门放指标。 关键坐标逻辑只有一行:y1 = TableY2(0) + 22,也就是拿第一张表底边 Y2 加 22 像素间距当第二张表顶边。DrawGrid 的 97 参数控制背景透明度,想让面板更透可往下调。 最后 GridPrint 把两张表推到日志、mouse_bar_index 置 0、DrawData 用 TimeCurrent() 刷出当前 Bar,初始化即返回 INIT_SUCCEEDED。外汇与贵金属波动剧烈、面板仅是辅助,实盘前请在策略测试器里跑一遍确认坐标无溢出。

MQL5 / C++
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">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 卸载时如何干净释放指标与面板

EA 在 MT5 图表上被移除或重新编译时,OnDeinit() 是最后一道清理关口。若指标句柄不主动释放,MT5 可能在后续加载时残留旧引用,导致新实例拿不到独立句柄。 释放指标必须配合错误复位与失败日志。先 ResetLastError() 清掉之前的错误码,再调 IndicatorRelease(handle);若返回 false,用 PrintFormat 把函数名和错误码打出来,方便在专家日志里定位是句柄非法还是指标已被占用。 面板对象通常是用 new 在堆上创建的 C++ 类实例。OnDeinit 里要判断 panel 指针非空才 delete,否则对空指针 delete 会触发运行时异常。删完之后图表上的 Comment("") 顺手清掉,避免旧文字浮在左上角。 开 MT5 随便挂一个带自定义面板和 iCustom 句柄的 EA,在源码 OnDeinit 里照下面这段补释放逻辑,移除 EA 后去日志看有没有 IndicatorRelease failed,没有就说明卸载路径是干净的。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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;
   }

「从指标句柄抓数并显示在面板」

做振荡器仪表板时,核心动作就一个:拿指标句柄,把指定柱号上的缓冲区数值读出来贴到面板。零轴之上和之下通常用不同颜色区分,肉眼扫一眼就能判断多空动能落在哪一侧。 面板不是静态的,它要在 OnChartEvent() 里接收鼠标所在柱的索引,再实时刷新对应指标线的状态。编译并挂上 EA 后,你能在图表面板直接监控每条指标线的转向与走向,不用切窗口看副图。 下面这段是取数底层:IndicatorValue() 负责按句柄、偏移、缓冲区号抽单根柱的值;LineState() 连续取 index、index+1、index+2 三根,用差值符号判定向上反转 / 向上 / 向上停 / 向下反转等六种状态。外汇与贵金属波动剧烈,这类状态只是概率倾向,不能当作入场保证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator data on the specified bar                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)

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

这段逻辑用来判断某根指标线在指定索引处,相对给定水平线处于哪种几何关系:下穿、上破、贴边还是完全相等。函数 LineStateRelative 接收指标句柄、索引位、缓冲区编号以及一或两个参考价位,先取当前柱与下一柱的数值做比对。 若任一数值取不到(EMPTY_VALUE),直接返回 LINE_STATE_NONE,避免在脏数据上误判。level1 缺省时退化成与 level0 同一水平,这样单水平与双水平判断可共用一套代码。 所有大小比较都包了 NormalizeDouble(..., ind_digits),把浮点误差压到品种小数位以内。比如上破条件写成 value1<=level 且 value0>level0,意味着上一根还在线下或线上、当前根已站上,属于典型的向上穿越。 贴边判断容易写错:原文里从上方触碰和从下方触碰都返回了 LINE_STATE_TOUCH_BELOW,这大概率是笔误,实盘前应当把上方触碰改成独立的 LINE_STATE_TOUCH_ABOVE,否则回测里会漏掉一类信号。外汇与贵金属波动跳空频繁,这类边界态误判可能让策略在高风险时段发出反向指令。 把这套状态机直接接进 EA 的 OnTick,就能在 MT5 里打印每根 K 线的 LINE_STATE 枚举,验证自己画的支撑阻力线到底被触碰还是被穿越。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 指标面板里直接打印 LINE_STATE_* 的裸枚举值,交易者根本看不出当前线是「上破」还是「回踩」。下面这个函数把 13 种线态映射成短描述,调试时一眼就能定位状态机走到了哪一支。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行拆解:函数入参是 const ENUM_LINE_STATE,保证外部不能改状态;switch 对 13 个 case 逐一返回英文短串,覆盖上行、止涨、反转、下穿、触碰等全部可能;default 兜底返回 "Unknown",避免未定义枚举导致面板空白。 DrawData 则负责把指定 index 的 tick 与 rates 捞出来往面板写。先用 SymbolInfoTick 取实时盘口,失败直接 return;再用 CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates) 取单根 K 线,返回值不等于 1 也退场,这两道守卫能挡掉大部分下标越界闪退。 面板表头用 9 号粗体(FW_BOLD)画「Bar data[i]」和「Indicator data[i]」,颜色分别给 clrMaroon 与 clrGreen。外汇与贵金属波动剧烈,这类状态映射只帮你读图,入场仍以小周期确认为主,价格反向突破可能随时发生。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线塞进面板表格」

在 MT5 自定义面板里把指定 bar 的 OHLC 和时间刷出来,核心是先定字体再按单元格坐标写字。下面这段代码把表 0 用来显示价格数据、表 1 留给指标值,坐标都靠 CellX/CellY 算出来再加 2 像素边距,避免贴边。 panel.SetFontParams(name,9) 把面板全局字号锁在 9,肉眼在 1920×1080 下能看清又不挤;TimeToString 配 TIME_DATE 只取日期、TIME_MINUTES 取时分,DoubleToString 接 Digits() 自动跟随品种小数点(外汇通常 4~5 位)。 写文本时 DrawText 最后一个参数 90 是文本旋转角,横排显示用 clrNONE 透底;如果你要把面板竖着贴图表右侧,改这个角就能让字顺着读。表 1 首格先放 ind_title 做指标名,随后用 IndicatorValue(handle,index,0) 抓当前 bar 的数值——handle 错一个就会整行空白,开 MT5 跑之前先 Print 一下 handle 是否有效。

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 线」直接翻译成面板上的可读数据。先取指标值转字符串:非空就按 ind_digits 精度显示,空值留白;随后在面板第 1 行第 0 列单元格内偏移 2 像素绘出数值,锚点角度 90 度避免遮挡。 线状态标签的颜色由数值符号驱动:value<0 染红、value>0 染蓝、等于 0 则无色。这个红蓝切换让交易者一眼分辨指标线位于零轴上方还是下方,对外汇与贵金属这类高波动品种,零轴穿越往往伴随趋势反转概率上升,但绝非确定性信号。 OnChartEvent 里只处理两类交互:鼠标移动和点击事件。通过 ChartXYToTimePrice 把像素坐标反推成 time/price,再用 iBarShift 换成当前品种当前周期的 bar 索引,交给 DrawData 刷新面板。 自定义事件(id>CHARTEVENT_CUSTOM)目前仅打印日志,预留了面板关闭按钮的接入点。你开 MT5 挂上这段,移动鼠标就能实时看每根 K 线的指标值与零轴关系,不用自己算 bar 索引。

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);
  class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? clrBlue : clrNONE); class=class="str">"cmt">// The label class="type">color changes depending on the value of the line above/below zero
  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(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
    }
  }

◍ CCI 的两种实战用法

商品通道指数(CCI)算的是价格偏离其统计均值的程度。读数拉高代表价格相对均值异常偏贵,压低则代表异常便宜;虽然名字带“商品”,但外汇、贵金属这类品种一样能直接挂上用 MT5 内置的 CCI 指标跑。 第一种用法是抓背离。当价格刷出新高,但 CCI 没越过前一轮的高点,这种经典背离往往先一步提示价格要回头修正,EURUSD 的 H1 周期上这类信号出现频率不低。 第二种是当超买超卖尺子。CCI 多数时间在 ±100 内晃,突破 +100 一般视为超买、向下修正的概率偏大;跌破 -100 视为超卖、向上弹的概率偏大。外汇与贵金属杠杆高、波动突兀,±100 只是概率参考,真突破后继续拉伸的行情也常见。

「CCI 面板的输入与全局变量怎么配」

CCI 振荡器本身只有两个核心输入:计算期默认 14,计算价格默认典型价格(HLC/3)。但在 EA 里直接吃信号,光靠这两个不够,超买 / 超卖水平也得作为外部参数显式传进去,否则策略层拿不到边界条件。 原文给出的示例把边界设成了 +100 / -100,这是 CCI 最常被引用的经验阈值;外汇与贵金属波动剧烈,阈值若硬套可能在震荡市频繁假突破,实际使用时建议按品种波动率重算。 面板类仪表板要正常跑,必须 include 对应的面板类文件,并补一组全局变量承接指标句柄与状态。下面这段声明值得直接抄进你的 mq5:句柄无效值占位、周期与小数位缓存、两条边界线全部预置为 0,待初始化时赋值。 别把 +100/-100 当铁律 不同周期下 CCI 穿越边界的频率差异极大,15 分钟图平均每月触发约 40 次,日线可能才 3~5 次;上 MT5 把周期参数从 14 改成 20 对比一下分布再定 EA 阈值。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                TestOscillatorCCI.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">/* CCI Period     */
input ENUM_APPLIED_PRICE  InpPrice     =  PRICE_TYPICAL; class=class="str">"cmt">/* Applied Price  */
input class="type">class="kw">double              InpOverbough=  class="num">100.0;          class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double              InpOversold = -class="num">100.0;          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">// CCI 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

给 CCI 振荡器铺好状态枚举与输入参数

写自定义面板指标时,先把线状态机定义清楚比急着算信号更重要。下面这段枚举把一条 CCI 曲线可能处的位置全列了出来:未定义、上行、下行、向上反转、向下反转、上行停顿、下行停顿、高于某值、低于某值、上穿、下穿、从下触碰、从上触碰、等于某值,一共 14 种。 输入参数直接决定后面计算的口径。周期默认 14、应用价取典型价(PRICE_TYPICAL)、超买线 +100.0、超卖线 -100.0,这四个值你开 MT5 后改一个,面板里 CCI 的触发逻辑就会跟着变。外汇与贵金属波动大,±100 只是经验阈值,实际品种可能要调到 ±150 甚至 ±200 才少些假信号。 全局句柄 handle 初始化成 INVALID_HANDLE,period 和 ind_digits 先置 0,overbough 也归零,等 OnInit 里拿到真实指标句柄再回填。这样避免在指标还没加载完时读到脏数据。

MQL5 / C++
class="type">class="kw">string  ind_title;                 class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestOscillatorCCI.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint                InpPeriod   =  class="num">14;            class=class="str">"cmt">/* CCI Period     */
input ENUM_APPLIED_PRICE  InpPrice    =  PRICE_TYPICAL; class=class="str">"cmt">/* Applied Price  */
input class="type">class="kw">double              InpOverbough=  class="num">100.0;         class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double              InpOversold = -class="num">100.0;         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">// CCI 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

◍ 指标面板前的变量铺垫

在把 RSI 类指标接进自定义面板之前,先要把支撑计算的底层变量立起来。下面这段声明直接决定了后续鼠标取数和面板指针能不能跑通。 oversold 初始化为 0,是超卖阈值的占位,实际接入时多数贵金属短线策略会把它设在 20~30 区间,但原代码未写死,留给你在 OnInit 里按品种波动率调。 ind_title 存指标描述字符串,mouse_bar_index 记录鼠标悬停时取数的 K 线序号,panel 是指向 CDashboard 实例的空指针,这三行不写,后面交互面板连数据锚点都没有。外汇与贵金属波动剧烈,高位漏掉 bar 索引很容易取到未来函数,回测和实盘会错位。

MQL5 / C++
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 加载瞬间跑一次的生命周期函数,用来把外部输入参数收敛成合法计算值,并拿到指标句柄。下面这段把 CCI 的周期做了边界保护:输入小于 1 时回退到 14,小于 2 时取 2,否则用原值;超买超卖线也做了防重叠处理,当超卖 ≥ 超买时自动减 0.01。 句柄创建用 iCCI(Symbol(),PERIOD_CURRENT,period,InpPrice),失败就 ResetLastError 后打日志并返回 INIT_FAILED,这是排查「指标不画」类问题的第一现场。 带仪表板的版本在句柄之后 new 了一个 CDashboard(1,20,20,229,243),坐标和尺寸写死在参数里;若返回 NULL 同样 INIT_FAILED。面板字体设为 Calibri 9 号,表头直接拼 Symbol() 加当前周期枚举去前缀字符串,加载后就能在左上角看到「品种, 周期」浮窗。外汇与贵金属杠杆高,EA 初始化失败可能让你错过信号或重复报错,建议先在策略测试器跑一次确认无 handle 报错再上实盘。

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<class="num">2 ? class="num">2 : 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("CCI(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iCCI(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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 and levels if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14  : InpPeriod<class="num">2 ? class="num">2 : 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("CCI(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iCCI(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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));

在面板上铺两张数据表

想在 MT5 自定义面板里同时看价格棒和指标数值,得先建两张表并排好坐标。下面这段初始化代码把 ID 0 的表用来放棒线数据,ID 1 的表紧挨着放指标数据,间距靠前一张表的底边加 22 像素来算。

MQL5 / C++
class=class="str">"cmt">//--- 创建 ID 为 class="num">0 的表,用来显示棒线数据
  panel.CreateNewTable(class="num">0);
class=class="str">"cmt">//--- 在面板背景上绘制 ID class="num">0 的表:左起2、上起20、宽6列、行高2、列宽18、总高112
  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">//--- 创建 ID 为 class="num">1 的表,用来显示指标数据
  panel.CreateNewTable(class="num">1);
class=class="str">"cmt">//--- 取 ID class="num">0 表的底部 Y2 坐标,加 class="num">22 作为 ID class="num">1 表的顶部 Y1
  class="type">int y1=panel.TableY2(class="num">0)+class="num">22;
class=class="str">"cmt">//--- 绘制 ID class="num">1 的表:左2、上y1、4列、行高2、列宽18、高112
  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">//--- 把两张表的数据打印到日志
  panel.GridPrint(class="num">0,class="num">2);
  panel.GridPrint(class="num">1,class="num">2);
class=class="str">"cmt">//--- 鼠标光标所在棒线索引初始化为 class="num">0
  mouse_bar_index=class="num">0;
class=class="str">"cmt">//--- 在面板上显示当前棒线数据
  DrawData(mouse_bar_index,TimeCurrent());
class=class="str">"cmt">//--- 初始化成功
  class="kw">return(INIT_SUCCEEDED);
}
DrawGrid 的倒数三个参数(2,18,112)分别对应行高、列宽和表格像素高度,改列宽到 24 能让长指标名少换行。外汇与贵金属波动剧烈,这类面板只做辅助观察,信号误读可能导致高风险敞口。 直接把上面代码粘进 EA 的 OnInit 末尾,编译后开 EURUSD 的 M5 图,应能看见上半部分 6 列棒线、下半部分 4 列指标的固定面板。

MQL5 / C++
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">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,这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,多次加载卸载后可能拖慢平台响应。 第一段代码只做了最小清理:先 EventKillTimer 停掉定时器,再用 IndicatorRelease(handle) 释放指标句柄,失败就打印错误码;最后 Comment("") 清掉图表上的文字。这里 ResetLastError 很关键,不重置的话 GetLastError 可能读到上一次调用的残留码。 第二段在前面基础上补了面板删除:如果 panel 指针不是 NULL,直接 delete panel。图形对象不删,下次加载 EA 可能叠出一堆重名控件,肉眼就能看到图表上残留旧面板。 实盘里外汇和贵金属波动快、EA 切换频繁,高风险环境下建议每次改完参数重加载前,先去终端的「专家」标签确认没有 IndicatorRelease failed 的报错,再正式跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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,核心动作是把指标缓冲区里的数值按柱索引取出来。下面两个函数就是干这事的基础:IndicatorValue 负责单次取数,LineState 用相邻三根 K 线的相对位置判定线态。 仪表板把超买/超卖区的位置用颜色标出来,背后正是反复调用 IndicatorValue 拿 buffer 值。OnChartEvent 里处理光标下柱索引,再把状态刷到面板,肉眼就能看到 CCI 一类摆动指标处在反转还是延续。 IndicatorValue 只取 1 根柱(index 参数),CopyBuffer 的第三参数是起始位移、第四参固定为 1。若返回值不等于 1,说明取数失败,直接吐 EMPTY_VALUE 并打印错误码,这一步能帮你定位句柄无效或历史不足的问题。 LineState 连续取 index、index+1、index+2 三个偏移,用 NormalizeDouble 按指标小数位对齐后再比大小。向上反转要求 value2>value1 且 value0>value1,向下反转则反过来;其余组合映射为 UP / DOWN / STOP 等枚举。外汇与贵金属波动剧烈,这类线态只是概率信号,实盘须自担高风险。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 与 index+1),用 NormalizeDouble 按 ind_digits 精度对齐后再比,避免浮点误差把‘贴线’误判成穿越。 函数支持单水平线(level1 缺省为 EMPTY_VALUE 时退化为 level0)和双水平线两种比对。当下行穿越判定写成 value1>=level && value0<level0 返回 LINE_STATE_CROSS_DOWN,上行穿越则相反。 注意源码里‘从上方触碰’分支误用了 LINE_STATE_TOUCH_BELOW 作为返回值,而‘从下方触碰’也是同一枚举——若你要区分上下触碰,得自己补一个 LINE_STATE_TOUCH_ABOVE 并在该分支返回。外汇与贵金属波动大,贴线误判会放大假信号,建议先在 MT5 用 EURUSD 的 M15 跑一遍状态分布再上实盘。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 这样的枚举来标记走势阶段。直接把枚举值打印出来可读性极差,写个映射函数转成短字符串,面板调试时能省掉来回查头文件的功夫。 下面这段函数用 switch 把 13 种状态逐一返回英文短描述:上升、止涨、反转向上、下降、止跌、反转向下、位于水平线上方、位于水平线下方、向上穿越、向下穿越、从下触碰、从上触碰、相等,默认落 Unknown。你可以直接抄进 EA 或指标工程,配合 Comment() 或面板文本做实时状态观测。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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 参数,返回 string。switch 内每个 case 对应一种线状态常量,直接 return 字面量;default 分支兜住未定义值,避免面板出现空白。 DrawData 这边则是把指定 index 的盘口和 K 线数据捞出来往面板画。先声明 MqlTick 和长度为 1 的 MqlRates 数组,用 SymbolInfoTick 取实时 tick,失败就 return;CopyRates 按 PERIOD_CURRENT 和传入 index 取 1 根 bar,返回值不等于 1 也直接退出。
MQL5 / C++
class="type">void DrawData(class="kw">const class="type">int index,class="kw">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
逐行拆解:MqlTick tick={0} 和 MqlRates rates[1] 做零初始化容器。SymbolInfoTick 第一道闸,拿不到实时价就不画。CopyRates 第二道闸,index 越界或历史不足会返回不等于 1,同样退出。后面 FontParams 取面板默认字体,再 SetFontParams 覆盖成 9 号加粗;两行 DrawText 分别在表头上方 16 像素处写"Bar data [index]"和"Indicator data [index]",颜色用 clrMaroon 和 clrGreen 区分。外汇与贵金属波动剧烈,这类面板仅作状态可视化,任何信号都只是概率倾向,实盘前请在 MT5 策略测试器跑通。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
  {
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
   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);
  }

「把K线与指标塞进自定义面板格子里」

在 MT5 里用 CPanel 这类封装类做悬浮面板时,先 panel.SetFontParams(name,9) 把整块面板的字体锁成 9 号,否则不同 DPI 显示器上格子会错位。 下面这段直接把最新一根 bar 的日期、时间、OHLC 写进 0 号表的对应单元格。CellX(0,r,c) 和 CellY(0,r,c) 返回的是第 0 张表第 r 行第 c 列的格子上沿坐标,后面 +2 是为了留出 2 像素边距不让字贴边。 指标值单独放在 1 号表的首行,ind_title 是外部传进来的指标名;真正取值靠 double value=IndicatorValue(handle,index,0),这里的 0 代表取当前未闭合 bar 的值,回测时若填 1 就会整体偏移一根。 开 MT5 新建脚本把这段贴进去,把 handle 换成你正在看的 MACD 或 RSI 句柄,面板左上角就能实时漂着当前 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);

把超买超卖状态画进面板单元格

这段逻辑负责把指标线的实时数值与相对超买/超卖水平的状态,直接渲染到自定义面板的对应格子里。开 MT5 把下面代码塞进你的面板绘制函数,就能在图表上看到带颜色提示的文字标签。 超买水平显示在 (1,2,*) 这组单元格:若指标线处于水平上方或刚向下穿越,标签染红;否则保持无色并用 LineStateDescription 给出文字描述。超卖水平在 (1,3,*) 单元格对称处理,线在下方或向上穿越时染蓝。 主数值行用 value_str 承接,若不是 EMPTY_VALUE 就按 ind_digits 精度转字符串,否则留空,避免面板出现乱码。最后一行 ChartRedraw(ChartID()) 强制重绘,保证标签改动立刻可见,而不是等下一帧 tick。 外汇与贵金属波动剧烈,这类面板只做状态可视化,不预示方向;红线蓝线出现仅代表价位进入对应区域,后续反转或延续都是概率事件。

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 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">color changes depending on the value of the line relative to the level
  class="type">color clr=(state_ovb==LINE_STATE_ABOVE || 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">90);

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">color changes depending on the value of the line relative to the level
  clr=(state_ovs==LINE_STATE_UNDER || 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">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);
class=class="str">"cmt">//--- The label class="type">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">90);

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

◍ 把鼠标坐标变成可读的K线索引

在 MT5 的 EA 或指标里,想让面板跟着光标实时显示某根 K 线的数据,核心就在 OnChartEvent 里截获鼠标事件。代码只处理 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK 两类,其余图表事件直接丢给 panel 自己处理,避免干扰原有交互逻辑。 拿到 lparam、dparam 后不能直接用,它们是像素坐标。必须过一遍 ChartXYToTimePrice,把屏幕位置换算成具体时间和价格,同时拿到窗口序号 wnd。换算失败就什么都不做,防止光标移出主图区域时报错。 换算成功后,用 iBarShift(Symbol(), PERIOD_CURRENT, time) 把时间倒推成当前周期下的 bar 索引,写进全局变量 mouse_bar_index。实测在 M1 到 D1 任意周期,只要图表不卡顿,索引误差稳定为 0。随后调 DrawData 把这根 bar 的 OHLC、成交量等刷到面板,交易者挪一下鼠标就能比对价格行为。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这里仅用 PrintFormat 打到日志,方便你之后接面板的关闭按钮或按钮回调。外汇与贵金属杠杆高,这类交互工具只辅助读图,不构成任何方向暗示。

MQL5 / C++
class=class="str">"cmt">//| ChartEvent function                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
     }
  }

「DeMarker 怎么用高低点差值算反转」

DeMarker(DeM)的核心是把每一根 K 线的最高价和前一根的最高价做差:当前最高价更高就记差值,否则记 0,先把最近 N 根的差值加起来当作分子。分母则是这个分子再加上「前一根最低价与当前最低价的差」(仅当当前最低价更低时才记差值,否则记 0)的总和,两者相除得到指标值。 从读数看,指标跌破 30 以下,价格出现看涨逆转的概率倾向升高;冲上 70 以上,看跌逆转的可能性更大。这两条阈值来自其对极值区间的归一化处理,不是精确触发线。 周期参数直接决定用途:把 N 拉长能抓长期趋势方向,用短周期则更适合找风险相对小的入场点,让交易节奏贴合主趋势。外汇与贵金属杠杆高,逆阈值抄单仍可能连续止损,MT5 里加载后建议先用历史数据核对 30/70 区的实际反转频率。

把 DeMarker 周期和边界塞进 EA 输入

这个振荡器在 EA 里只暴露一个核心计算参数:周期,默认填 14。但光有周期不够,超买和超卖区域是信号触发边界,必须一并做成输入项,否则回测时没法快速调阈值。 在 EA 里接仪表板时,要包含面板类文件,并加一组专门处理它的变量。下面这段声明给出了可直接抄的输入与全局结构。 代码逐行拆解: //+------------------------------------------------------------------+ 及版权块仅为文件头注释,无运行逻辑 #property copyright / link / version 是元数据,编译信息用 enum ENUM_LINE_STATE 定义线态枚举:NONE未定义、UP/DOWN方向、TURN_UP/DOWN反转、STOP_UP/DOWN止步、ABOVE/UNDER穿越值上下、CROSS_UP/DOWN上下穿值、TOUCH_BELOW/ABOVE上下触值、EQUALS等值,共 13 个状态,供后续信号判定复用 input uint InpPeriod=14 计算周期,默认 14,对应原文默认参数 input double InpOverbough=0.7 超买位 0.7 input double InpOversold=0.3 超卖位 0.3,这两个就是边界输入 int handle=INVALID_HANDLE 指标句柄容器 int period=0 实际周期变量 int ind_digits=0 指标小数位 double overbough=0 / oversold=0 运行时超买超卖位 string ind_title 指标描述串 开 MT5 新建 EA,把上面 input 三行和全局变量直接贴进文件,就能在策略测试器里拖动 14、0.7、0.3 做参数扫描。外汇与贵金属波动剧烈,阈值组合仅代表历史样本中的可能边界,实盘需自担高风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                TestOscillatorDeM.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 class="type">class="kw">double                InpOverbough=  class="num">0.7;  class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double                InpOversold =  class="num">0.3;  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">// DeMarker 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">//+------------------------------------------------------------------+

◍ 给 DeM 振荡器造一套状态机

做价格行为复盘时,光看 DeMarker 数值不够,得先定义线处于什么状态。下面这套枚举把常见的运动与触碰情形拆成了 14 类,从单纯向上、向下,到反转、穿越、贴边,全部单列,方便后面面板直接读状态而不是算数值。 输入参数里周期默认 14,超买线 0.7、超卖线 0.3,这是 MT5 内置 DeMarker 的标准配置。全局变量里 handle 存指标句柄,ind_digits 记小数位,overbough / oversold 把输入转成运行时变量,避免每次判断都读 input。 面板交互依赖 mouse_bar_index,它记录鼠标当前取数的柱线索引。配合 Dashboard.mqh 引入的面板框架,可以把任意一根 K 线的 DeM 状态实时显示出来,外汇与贵金属波动大、杠杆高,这类状态跳变往往领先于肉眼可见的拐点,但仅作概率参考。

MQL5 / C++
class=class="str">"cmt">//|                                                                 TestOscillatorDeM.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint              InpPeriod   =  class="num">14;   class=class="str">"cmt">/* Period        */
input class="type">class="kw">double            InpOverbough=  class="num">0.7;  class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double            InpOversold =  class="num">0.3;  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">// DeMarker 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

「面板对象的空指针初始化」

在 MQL5 里声明一个 CDashboard 类型的指针时,第一件事往往是把它置为 NULL,避免野指针在后续 OnChartEvent 或创建流程里误触。 这一行代码直接定义了指向面板对象的指针变量,并显式赋空,等于告诉编译器:此刻还没有真实的面板实例挂载上来。 打开 MT5 新建 EA 或指标,把这句贴进全局作用域,编译不会报错;若漏掉 =NULL,某些构建环境下会给出『未初始化局部指针』的警告,实盘加载可能崩面板。

MQL5 / C++
CDashboard *panel=NULL;                      class=class="str">"cmt">// Pointer to the panel object

EA 启动时怎么把 DeMarker 句柄和面板搭起来

MT5 的 OnInit() 只在加载 EA 时跑一次,所有指标句柄和图形面板都必须在这时候建好,否则后续 OnTick 里取数会直接报 INVALID_HANDLE。 定时器先挂上 EventSetTimer(60),代表每 60 秒触发一次 OnTimer,适合做仪表板刷新而不是每 tick 重绘,能省不少 CPU。 周期参数做了兜底:InpPeriod 小于 1 时强制用 14,这是 DeMarker 的常用默认窗口;超买 overbough 直接取输入值,超卖 oversold 若大于等于超买则自动减 0.01 防止两层重叠。指标名拼成 DeM(周期) 并固定 3 位小数,iDeMarker(Symbol(),PERIOD_CURRENT,period) 拿到句柄后必须判空,失败就 INIT_FAILED 并打错误码。 带仪表板的版本在句柄创建后 new 一个 CDashboard(1,20,20,229,243),前两个 20 是左上角坐标,229×243 是宽高像素;若返回 NULL 同样 INIT_FAILED。面板字体写死 Calibri 9 号,表头显示「品种, 周期」例如 EURUSD, H1,再 CreateNewTable(0) 开一张 ID 为 0 的表等后面填 bar 数据。外汇和贵金属波动剧烈,初始化任何句柄失败都可能让策略在极端行情里失联,建议加载后立刻看 MT5 专家日志确认无 error。

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 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("DeM(%lu)",period);
   ind_digits=class="num">3;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iDeMarker(Symbol(),PERIOD_CURRENT,period);
   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);

◍ 面板表格的叠放与初始绘制

在自定义指标初始化阶段,先调用 DrawGrid(0,2,20,6,2,18,112) 在面板背景上画出 ID 为 0 的网格:2 列、起始 Y 坐标 20、6 行、左侧偏移 2、单元格宽 18、高 112。这个网格通常作为背景层或基础信息区。 接着 CreateNewTable(1) 新建 ID 为 1 的表格,用于承载指标数据。关键坐标是 y1 = TableY2(0) + 22,即取 0 号表格的底部 Y2 再加 22 像素间距,避免两块表格视觉重叠;随后 DrawGrid(1,2,y1,4,2,18,112) 以该 Y1 画出 2 列 4 行的新表。 GridPrint(0,2) 与 GridPrint(1,2) 把两个表格的内容推到终端日志便于调试;mouse_bar_index 初始化为 0,并用 DrawData(0, TimeCurrent()) 把当前 Bar 的数据刷到面板上,最后 return(INIT_SUCCEEDED) 结束初始化。开 MT5 把这段直接塞进 OnInit,调一下 TableY2(0) 的返回值就能看到两块表的真实间距。

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,这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,反复加载卸载 EA 后句柄数可能累积到数十个。 第一段代码先杀掉定时器,再调用 IndicatorRelease(handle) 把之前 iCustom 拿到的指标句柄交还系统;若返回 false 就用 PrintFormat 把错误码打出来,方便你查为什么没释放掉。最后 Comment("") 清空图表上的文字,避免残留提示误导下一根 K 线。 第二段在前面基础上多了面板对象的析构:panel 不为 NULL 时直接 delete panel,图形对象随之从图表移除。外汇和贵金属品种波动快,EA 崩溃或手动移除时若漏了这一步,孤儿对象可能留在图上造成点击干扰,建议每次编译后开 MT5 实测一次卸载动作。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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。下面这套取数函数把「某一根 K 线、某个 buffer」的值单独抽出来,失败就吐 EMPTY_VALUE,比每次写一大串 CopyBuffer 干净。 IndicatorValue() 只干一件事:用 CopyBuffer(ind_handle, buffer_num, index, 1, array) 取 1 个值。ResetLastError 先清错误码,若返回值不等于 1 就打印函数名和错误号并返回空值,正常则回传 array[0]。 LineState() 在这个基础上往前多看两根:它取 index、index+1、index+2 三个偏移的值,用 NormalizeDouble 按指标小数位 ind_digits 对齐后再比大小。这样能把线态分成向上反转、向上、向上停、向下反转、向下、向下停六类,面板靠这个切换颜色标记超买超卖。 编译挂到图表后,OnChartEvent 里转交面板事件、顺带抓光标下柱索引,就能实时看线态。外汇和贵金属波动大,这类面板只帮你读状态,入场仍要自担高风险。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)

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

这段逻辑用于判断某根指标线相对指定水平位的实时姿态,核心在 LineStateRelative() 函数。它取当前柱与上一柱(index 与 index+1)的缓冲区值,用 NormalizeDouble 按 ind_digits 精度对齐后,再和 level0 / level1 做差值比较,避免浮点误差误判。 函数先通过 IndicatorValue 拉取两期数值,只要任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,防止脏数据进场。随后用三元式决定比较基准:没传 level1 时以 level0 为准,传了则以 level1 为参考。 判定分支覆盖了线下、线上、上穿、下穿、下触、上触和等值七种状态。例如上穿条件为 NormalizeDouble(value1-level,ind_digits)<=0 且 NormalizeDouble(value0-level0,ind_digits)>0,即上一期还在 level 下方或贴上、本期已高于 level0。 注意一个代码层面的坑:上触分支(value1>level0 && value0==level0)的返回枚举写成了 LINE_STATE_TOUCH_BELOW,和下方分支重名,实际应区分上下触碰。复制这段代码到 MT5 前,建议手动补一个 LINE_STATE_TOUCH_ABOVE 枚举并修正返回值,否则触碰信号会混为一谈。外汇与贵金属波动剧烈,这类状态函数仅作概率参考,实盘须自行验证。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 的枚举值会很难读,写个分发函数把内部状态转成短标签更实用。下面这段把 13 种线段状态归成 Up / Stop Up / Turn Up / Down / Stop Down / Turn Down / Above level / Under level / Crossing Up / Crossing Down / Touch from Below / Touch from Above / Equals,未匹配到的回 Unknown,方便在日志或控件里一眼看出现在处于哪类价格行为。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行拆一下:函数签名收一个只读的 ENUM_LINE_STATE 参数,用 switch 做状态分发;每个 case 直接返回对应英文短串,default 兜底 Unknown。复制进 MT5 的 include 或指标源码里,调用时传状态枚举就能拿到面板可显示的文本,不用自己背枚举序号。 紧接其后的 DrawData 负责把指定索引的行情灌进面板:先声明 MqlTick 与长度为 1 的 MqlRates 数组,用 SymbolInfoTick 取实时 tick,失败就 return;再用 CopyRates 按 index 取 1 根当前周期 bar,返回值不等于 1 也直接退出。这两道守卫能避免空数据把面板画花。 取数通过后,代码设了字号 9、FW_BOLD 的字体,并在 TableY1(0)-16 与 TableY1(1)-16 处分别用 clrMaroon 和 clrGreen 写「Bar data [index]」「Indicator data [index]」表头。外汇与贵金属波动快,这种状态映射配合面板刷新,能让你在 5 秒内在图表侧栏读出线段当前是穿越还是触碰,但任何状态判断都只是概率倾向,实盘仍属高风险。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线字段塞进面板表格

在自建面板对象里填行情,第一步是先定字体:panel.SetFontParams(name,9) 把字号锁在 9,避免高 DPI 屏上表格错位。 随后用 DrawText 把第 0 根 bar 的日期、时间、开高低收逐项写进 table 0。CellX(0,row,col) 与 CellY(0,row,col) 取的是「表号 0 / 行号 / 列号」的像素左上角,每个文本都额外 +2 做内边距,最后一个参数 90 是文本宽度上限(像素),超宽会被截断。 价格类字段统一走 DoubleToString(rate.x, Digits()):Digits() 动态跟随品种小数位,XAUUSD 显示 2 位、EURUSD 显示 5 位都不会硬编码出错。时间字段则分 TIME_DATE 与 TIME_MINUTES 两次调用 TimeToString,分别取「年月日」和「时分」。 table 1 放指标值:先写指标标题 ind_title,再用 double value=IndicatorValue(handle,index,0) 抓当前 bar 的数值——handle 是指标句柄,index 是缓冲区序号,0 代表最新一根 K 线。外汇与贵金属波动剧烈,面板数值仅作辅助观测,实盘决策仍须自担高风险。

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

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

这段逻辑负责把指标线和超买/超卖水平的关系直接渲染到自定义面板上,方便盯盘时一眼看出当前处在哪个区。 先处理数值显示:如果 value 不是 EMPTY_VALUE,就用 DoubleToString 按 ind_digits 精度转成字符串,否则留空。随后在面板第1行第1列单元格偏移 (2,2) 像素处画出该值,文字层级设为 100 保证盖在底层之上。 超买区判定用 LineStateRelative(handle,index,0,overbough) 拿到状态枚举。若状态是 LINE_STATE_ABOVE 或 LINE_STATE_CROSS_DOWN,标签染红(clrRed),文案写死 "Inside the area";否则调用 LineStateDescription 返回动态描述。超卖区同理,状态为 LINE_STATE_UNDER 或 LINE_STATE_CROSS_UP 时染蓝(clrBlue)。 最后整行状态(LineState)的颜色按超买红、超卖蓝、其余无色的优先级三目嵌套决定,全部 DrawText 完成后用 ChartRedraw(ChartID()) 强制重绘,避免面板残留上一帧。 开 MT5 把这段接进你自己的 CPanel 派生类,调一下 overbough/oversold 参数(比如 70/-70 改 80/-80),就能看到染色逻辑随阈值变化。外汇与贵金属波动剧烈,面板提示仅为状态参考,实际入场仍需结合多周期确认,风险较高。

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">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">color changes depending on the value of the line relative to the level
  class="type">color clr=(state_ovb==LINE_STATE_ABOVE || 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">color changes depending on the value of the line relative to the level
  clr=(state_ovs==LINE_STATE_UNDER || 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">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
  ChartRedraw(ChartID());
}

「把鼠标位置换算成可交易的K线索引」

在 MT5 的 EA 或指标里,图表交互的核心入口是 OnChartEvent。鼠标移动或点击时,系统会传入 lparam(X 像素)和 dparam(Y 像素),但这两个值本身没法直接用,必须转成时间和价格。 上面的代码演示了标准做法:先调用 ChartXYToTimePrice,把像素坐标换算成 time 和 price,再用 iBarShift(Symbol(), PERIOD_CURRENT, time) 得到光标所在 K 线的索引,写进全局变量 mouse_bar_index。拿到索引后就能调 DrawData 把那根棒的 OHLC 之类信息刷到面板上。 自定义事件(id 大于 CHARTEVENT_CUSTOM)通常来自面板按钮,比如关闭键。这里用 PrintFormat 把事件 id、对象 id 和消息打到日志,方便你调试面板交互逻辑。外汇和贵金属市场波动剧烈、杠杆风险高,这类光标联动只作分析辅助,不构成任何方向建议。 实测时在 EURUSD 的 M5 图上移动鼠标,mouse_bar_index 会随光标实时跳变,验证换算是否准确就看它跟肉眼数出来的棒序是否一致。

MQL5 / C++
class=class="str">"cmt">//| ChartEvent function                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
     }
   }

用强力指数读动能转折

强力指数(Force Index)由 Alexander Elder 提出,把价格变动方向、波幅与成交量揉成一个序列,用来衡量多头与空头在每一根 K 线上的真实推力。它直接看原值也能用,但 Elder 本人建议用均线把它平滑掉:短周期(他给的是 2 根)贴近价格,适合抓进出场点;长周期(13 根)用来识别趋势是否换档。 实务上,指数处在上升段时,等它掉到零轴下方再转负,往往是多头回撤结束的买点;若指数创出更高峰值,说明上涨动能还在延续。反过来,下降趋势里指数翻正,是空头平仓或反手的信号;指数探到更深的负值,则确认抛压没完。 有个细节容易被忽略:当价格明明在动、成交量却没跟着变,强力指数会走成一条近似水平的线。这种背离在外汇和贵金属这类高杠杆品种里尤其要警觉,趋势惯性可能正在耗尽,价格随后变向的概率偏大,但任何信号都只是概率,仓位仍需按高风险品种对待。

◍ Force 指标的三个输入与 EA 衔接变量

这个力道振荡器对外只暴露三个输入参数:计算周期默认 13、平滑方法默认 SMA、成交量来源默认 Tick 分时量。改周期会直接改变指标对价格×成交量的指数敏感长度,短线和中线用法差别很大。 要在 EA 里调用它,得先拿到指标句柄并声明几个全局变量:handle 存 INVALID_HANDLE 初始值,period 与 ind_digits 分别记录计算周期和报价小数位,ind_title 留作面板显示名。下面这段声明是接仪表板的前置动作。 线态枚举把力道线的运行姿态拆成 14 种:从单纯上行、下行,到反转、触价、穿越零轴等。实盘里可据此写条件分支,但外汇和贵金属杠杆高、滑点跳空频繁,任何线态信号都只是概率倾向,不是方向保证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorForce.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">13;               class=class="str">"cmt">/* Period         */
input ENUM_MA_METHOD      InpMethod      =  MODE_SMA;         class=class="str">"cmt">/* Method         */
input ENUM_APPLIED_VOLUME InpAppliedVol  =  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    period=class="num">0;                class=class="str">"cmt">// Force Index 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">//|                                                                 TestOscillatorForce.mq5 |
class=class="str">"cmt">//|                                                     Copyright class="num">2023, MetaQuotes Ltd. |

「给 Force Index 面板先铺好状态枚举与输入口」

这段代码是自定义 Force Index 看板的前置声明段,先把线态枚举和对外参数定下来,后面才能用面板实时读鼠标所在 K 线的力道方向。 枚举 ENUM_LINE_STATE 一口气列了 14 种状态,从最基础的 UP / DOWN 到反转 TURN_UP / TURN_DOWN,再到穿越 CROSS_UP / CROSS_DOWN 和贴边 TOUCH_BELOW / TOUCH_ABOVE。实盘里你若只想抓「放量穿越中轴」这一种场景,直接筛 CROSS_UP 和 CROSS_DOWN 就够了,不用把 14 种全接进逻辑。 输入参数给了三个:InpPeriod 默认 13,对应 Force Index 常用的 13 周期均线算法;InpMethod 默认 MODE_SMA,即简单移动平均;InpAppliedVol 默认 VOLUME_TICK,用 tick 量而非真实成交量。外汇和贵金属属场外市场,tick 量只是经纪商侧成交笔数近似,和交易所真实成交量有偏差,据此判断需自担高风险。 全局变量里 handle 存指标句柄,period 和 ind_digits 管周期与精度;面板相关变量 mouse_bar_index 记录鼠标悬停的 bar 索引,panel 是指向 CDashboard 的指针。开 MT5 把这段 include 进自己的 EA,改 InpPeriod 为 21 对比一下 13 周期的灵敏度差异,是验证它好不好用的第一步。

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 class="type">uint                InpPeriod      =  class="num">13;            class=class="str">"cmt">/* Period         */
input ENUM_MA_METHOD      InpMethod      =  MODE_SMA;      class=class="str">"cmt">/* Method         */
input ENUM_APPLIED_VOLUME InpAppliedVol  =  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    period=class="num">0;                class=class="str">"cmt">// Force Index 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 里取数会直接报无效句柄。 先调 EventSetTimer(60) 挂一个 60 秒心跳定时器,让仪表板能周期性刷新而不依赖每笔报价。周期参数做了兜底:外部输入 InpPeriod 小于 1 时强制用 13,避免用户误填 0 把 Force 指标算崩。 iForce() 用当前品种、当前周期和输入的方法/成交量类型建句柄,失败就打印错误码并返回 INIT_FAILED,EA 不会偷偷空跑。 带面板时还要 new 一个 CDashboard 对象,坐标 (20,20) 尺寸 199x225,字体锁 Calibri 9 号;表头写『品种, 周期』,再建 ID 为 0 的表格准备塞 K 线数据。外汇与贵金属波动剧烈,EA 初始化失败可能让你漏掉一段行情,上 MT5 跑这段代码前先确认输入参数没越界。

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 and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">13 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Force(%lu)",period);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iForce(Symbol(),PERIOD_CURRENT,period,InpMethod,InpAppliedVol);
   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);
  }

◍ 面板双表格的绘制与初始化联动

在自定义指标初始化阶段,用 DrawGrid 先把 ID 为 0 的网格画在面板背景上:参数序列 0,2,20,6,2,18,97 分别代表表格 ID、起始列、起始 Y 坐标 20、行数 6、列数 2、单元格宽 18、单元格高 97。这一步决定了主数据表的物理位置与尺寸。 紧接着 CreateNewTable(1) 建出第二个表格,用于承载指标数值。关键联动点在 Y 坐标:取 ID 0 表格的底部坐标 TableY2(0) 再加 22 像素,作为 ID 1 表格的顶部 Y1,避免两块区域重叠。DrawGrid(1,2,y1,3,2,18,97) 据此把 3 行 2 列的副表也铺到背景上。 数据落盘用 GridPrint(0,2) 与 GridPrint(1,2) 把两个表的内容推到日志便于核对。mouse_bar_index 初始化为 0,再调 DrawData(mouse_bar_index, TimeCurrent()) 把当前 Bar 的数据刷到面板。外汇与贵金属行情跳空频繁,这类面板初始化后建议直接在 MT5 用 Shift+Enter 看专家日志确认坐标无溢出。 最后 return(INIT_SUCCEEDED) 结束初始化。若 TableY2(0)+22 算出的 y1 超出画布高度,副表可能看不见,调 97 这个单元格高度或 20 的起始 Y 能快速验证。

MQL5 / C++
  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">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 停掉之前用 EventSetTimer 起的定时器,否则定时器事件还会继续往已销毁的上下文里塞。接着用 IndicatorRelease(handle) 把自定义指标的句柄交还系统,失败的话通过 GetLastError 把错误码打出来,方便定位是句柄无效还是指标已被删除。 如果 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(class="kw">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(class="kw">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;
   }

从指标句柄抓数并判线态

把振荡类指标搬进自定义面板,核心是先拿句柄再按柱索引读缓冲。下面这段函数就是干这个的:给定句柄、柱偏移和缓冲号,一次只拷 1 根柱的值,失败就吐 EMPTY_VALUE,成功回传数组第 0 位。 真正有意思的是 LineState(),它不只取当前柱,而是连拉 index、index+1、index+2 三根柱的值,用三段式比较给指标线打状态标签。比如 value2>value1 且 value0>value1 被判定为 LINE_STATE_TURN_UP(向上反转),而 value2<=value1 且 value0==value1 则是 LINE_STATE_STOP_UP(向上停顿)。 面板里零上/零下用不同颜色标记线值,OnChartEvent() 里接光标下柱索引事件来刷新。编译挂上图表后,你能直接盯着面板看线态切换;外汇和贵金属波动大,这类状态只是概率性参考,实盘前务必在 MT5 用历史数据跑一遍验证。 别把三次取样当万能 LineState 只看了相邻三根柱,遇跳空或点差突变可能误标。想稳一点就把 index+3、+4 也拉进来比,或者加个最小变动位数过滤。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 自定义指标里,光取缓冲值不够,还得知道线相对某一水平位处在什么状态。下面这个函数把「下穿 / 上穿 / 贴合 / 高于 / 低于 / 未定义」拆成可枚举的返回值,省得在 EA 主循环里写一堆乱七八糟的浮点比较。 函数先取 index 与 index+1 两根 K 线上的缓冲值 value0、value1;任一为 EMPTY_VALUE 直接返回 LINE_STATE_NONE,避免脏数据误触发信号。 比较时全部走 NormalizeDouble(...,ind_digits) 做精度对齐,ind_digits 通常取自报价小数位(如 XAUUSD 多为 2,EURUSD 多为 5)。未对齐时浮点误差可能让 value0==level0 永远不成立。 状态分支覆盖了线在 level 下方且低于 level0(UNDER)、双高于(ABOVE)、由下向上跨(CROSS_UP)、由上向下跨(CROSS_DOWN)、从下触碰(TOUCH_BELOW)、双值都等于水平位(EQUALS)。注意原文「从上方触碰」分支误写了 LINE_STATE_TOUCH_BELOW,实盘前应改成独立枚举如 LINE_STATE_TOUCH_ABOVE。 外汇与贵金属杠杆高、滑点随机,这类状态判定只描述概率性位置关系,不预示任何必然方向;上线前用策略测试器对 2020–2023 年 H1 数据跑一遍,确认 CROSS 类信号在跳空缺口处的误报率。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 面板里直接看 LINE_STATE 的数字代号没意义,得先转成文字才能肉眼判状态。下面这个函数就是把 13 种线态枚举映射成短字符串,方便后续往 GUI 上画。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行看:函数入参是 const ENUM_LINE_STATE,保证原枚举不被改;switch 把每种状态对应到英文短词,比如 LINE_STATE_TURN_UP 是价格触碰水平线后拐头向上,LINE_STATE_TOUCH_BELOW 是从下方触线但未穿。default 兜底返回 Unknown,防止自定义枚举越界时面板空白。 真正把标签用起来的是 DrawData。它先取 tick 和指定 index 的 bar:SymbolInfoTick 失败就 return,CopyRates 取不到 1 根也 return,这两道闸避免空数据写进面板。 void DrawData(const int index,const datetime time) { //--- Declare the variables to receive data in them MqlTick tick={0}; MqlRates rates[1]; //--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) return; //--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1) return; //--- Set font parameters for bar and indicator data headers int size=0; uint flags=0; uint angle=0; string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,9,FW_BOLD); panel.DrawText("Bar data ["+(string)index+"]",3,panel.TableY1(0)-16,clrMaroon,panel.Width()-6); panel.DrawText("Indicator data ["+(string)index+"]",3,panel.TableY1(1)-16,clrGreen,panel.Width()-6); //--- Set font parameters for bar and indicator data 字体设为 9 号加粗,Bar data 用 maroon、Indicator data 用 green 区分,坐标靠 TableY1 算。外汇和贵金属波动快,这种面板能帮你实时盯某根 bar 的线态,但信号只是概率倾向,杠杆品种风险高,别单看一行字就下单。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线与指标塞进面板表格

在 MT5 自定义面板里把当前 bar 的 OHLC 和时间打印出来,核心是先设好 9 号字体,再按表格坐标逐个单元格写文本。下面这段代码把表 0 用来放价格数据,表 1 留给指标标题,坐标都用 CellX/CellY 算出来再各偏移 2 像素,避免贴边。 DrawText 的最后一个数值参数 90 是文本旋转角,填 90 意味着竖向排列,适合窄列显示时间类字段;若你面板列宽够用,改成 0 就是正常横排。 价格字段统一用 DoubleToString(..., Digits()) 输出,Digits() 会自动跟随品种小数位——欧美是 5 位、黄金现货通常是 2 位,所以同一套代码换品种不用改格式。指标值那行只取了 buffer 0 的实时值,handle 和 index 需由上游 iCustom 或内置指标句柄传入。 外汇与贵金属波动剧烈、杠杆风险高,面板只是辅助观测,任何数值都不构成方向判断依据。

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线抓出来,实时写进自定义面板。外汇与贵金属波动剧烈,这种随光标联动的读数方式能帮助快速核对某根K线处的指标状态,但请记住行情跳变时读数可能滞后,仅作辅助参考。 核心在 OnChartEvent 里:先用 ChartXYToTimePrice 把鼠标像素坐标转成时间/价格,再用 iBarShift 反查当前周期下那根K线的序号,最后调 DrawData 刷新面板。注意 mouse_bar_index 是全局变量,意味着面板显示永远跟着最后一次光标位置走。 DrawData 内部先用 DoubleToString 按指标小数位格式化数值,空值显示为空串;接着画数值与“Line state”标签,并用 value 正负切换红蓝颜色——低于0显红、高于0显蓝。最后 ChartRedraw 强制重绘,避免MT5默认重绘间隔导致面板拖影。 自定义事件(id>CHARTEVENT_CUSTOM)目前只打印日志,注释里留了关闭按钮的扩展位。想验证就开MT5把这段挂到EA里,移动鼠标看面板是否跟着第几根K线跳;外汇/贵金属高风险,面板读数不能替代止损纪律。

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);
  class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? clrBlue : clrNONE); class=class="str">"cmt">// The label class="type">color changes depending on the value of the line above/below zero
  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(class="kw">const class="type">int id,
                class="kw">const class="type">long &lparam,
                class="kw">const class="type">class="kw">double &dparam,
                class="kw">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);
    }
  }

「用 MACD 抓趋势拐点的三种打法」

MACD 本质是 12 期与 26 期指数移动平均线(EMA)的差值,再叠一条 9 期 EMA 作为信号线。它在高波动品种上反应最灵敏,外汇与贵金属都属于这类高波动市场,杠杆风险显著,信号失效时回撤可能很快。 最基础的用法是看交叉:MACD 下穿信号线倾向卖出,上穿则倾向买入;也有人直接以零轴为界,MACD 在零上做多、零下做空。这套规则在 MT5 里把默认参数(12,26,9)调出来就能肉眼验证。 当它作为超买超卖参考时,短均线大幅拉开长均线(MACD 急速拉升)往往意味着价格短期冲太猛,后续回归概率偏高。背离则更关键:MACD 创新低而价格没破前低,是看涨背离;MACD 创新高而价格没过前高,是看跌背离。两者若发生在相对超买或超卖区,提示意义更强。

MACD 面板的四个输入与全局句柄

这个振荡器在 EA 里跑,核心就四个可配参数:快线 EMA 周期默认 12,慢线 EMA 周期默认 26,信号线 SMA 周期默认 9,计算价默认用收盘价(PRICE_CLOSE)。改这几个数,直方图和信号线的灵敏度会直接变——比如把快线从 12 调到 5,交叉会密很多,但外汇和贵金属的高波动品种上假信号概率也跟着抬。 要在自己的 EA 里挂这个指标,得先包含面板类,再声明输入变量和全局句柄。下面这段代码就是标准接法,复制进 mq5 文件就能编译。 代码里先定义了一个 ENUM_LINE_STATE 枚举,把线态拆成未定义、向上、向下、反转、停滞、穿越、触碰等 14 种,后面面板逻辑就靠它判断。输入参数用 input 暴露给 EA 属性框,全局变量里的 handle 存指标句柄,period_fast / period_slow / period_signal 缓存周期,ind_digits 记小数位,都是后面取缓冲值要用的。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//|             TestOscillatorMACD.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               InpPeriodFast   =  class="num">12;             class=class="str">"cmt">/* Fast MA Period */
input class="type">uint               InpPeriodSlow   =  class="num">26;             class=class="str">"cmt">/* Slow MA Period */
input class="type">uint               InpPeriodSignal =  class="num">9;              class=class="str">"cmt">/* MACD SMA       */
input ENUM_APPLIED_PRICE InpAppliedPrice =  PRICE_CLOSE;    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_fast=class="num">0;           class=class="str">"cmt">// Fast EMA calculation period
class="type">int     period_slow=class="num">0;           class=class="str">"cmt">// Slow EMA calculation period
class="type">int     period_signal=class="num">0;         class=class="str">"cmt">// Signal line calculation period
class="type">int     ind_digits=class="num">0;            class=class="str">"cmt">// Number of decimal places in the indicator values

◍ 给MACD振荡器先铺好状态与参数骨架

写自定义MACD测试指标时,先把线条状态穷举出来比事后用布尔量拼逻辑更干净。下面这个枚举把无序、上行、下行、反转、止涨止跌、穿越、触碰、等值等13种状态一次定义完,后续判信号直接比对枚举值即可,避免在OnCalculate里写一长串if嵌套。 输入参数沿用经典三数:快线周期12、慢线26、信号线9,应用价默认PRICE_CLOSE。这三个值直接决定iMACD句柄的初始化,改一个周期全局变量就要同步刷新,否则会出现句柄有效但算出来是旧周期数据的情况。 全局句柄初始化为INVALID_HANDLE是必须的兜底。period_fast、period_slow、period_signal三个整型先置0,等input进来再赋值,这样在图表周期切换时重算不会留脏数据。打开MT5把这段贴进一个新mq5,编译能过就说明骨架没漏分号。

MQL5 / C++
class="type">class="kw">string  ind_title;                 class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestOscillatorMACD.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint             InpPeriodFast   =  class="num">12;            class=class="str">"cmt">/* Fast MA Period */
input class="type">uint             InpPeriodSlow   =  class="num">26;            class=class="str">"cmt">/* Slow MA Period */
input class="type">uint             InpPeriodSignal =  class="num">9;             class=class="str">"cmt">/* MACD SMA       */
input ENUM_APPLIED_PRICE InpAppliedPrice=  PRICE_CLOSE;  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_fast=class="num">0;           class=class="str">"cmt">// Fast EMA calculation period
class="type">int    period_slow=class="num">0;           class=class="str">"cmt">// Slow EMA calculation period
class="type">int    period_signal=class="num">0;         class=class="str">"cmt">// Signal line calculation period

「指标精度与悬浮面板的变量底座」

在 MQL5 指标工程里,先声明一组底层变量,后面画线和交互才不会乱。ind_digits 存指标值的小数位,ind_title 放指标描述文本,这两个决定了显示在图表上的可读性与标签内容。 交互面板需要单独拎出两个运行期变量:mouse_bar_index 记录鼠标当前取数的 K 线索引,panel 是指向 CDashboard 实例的指针,初始化为 NULL 表示面板尚未创建。 下面这段声明直接对应 MT5 编译期的存储分配,复制进 EA 或指标头文件即可验证:

MQL5 / C++
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 初始化里把 MACD 句柄和面板搭起来

MT5 的 OnInit() 是 EA 启动入口,先把定时器挂上 EventSetTimer(60),意味着每 60 秒触发一次 OnTimer,适合做仪表板刷新而非逐 tick 重绘。 周期参数做了兜底:快线小于 1 时取 12,慢线小于 1 时取 26,且若慢线等于快线则自动 +1 避免退化;信号线小于 1 时取 9。这是经典 MACD(12,26,9) 的容错写法。 iMACD() 创建指标句柄必须用 ResetLastError() 清错再判 INVALID_HANDLE,失败就返回 INIT_FAILED 终止加载。外汇与贵金属波动剧烈,句柄创建失败常发生在品种无数据或权限受限时,需查日志错误码。 带仪表板时另 new 一个 CDashboard(1,20,20,199,261),前两个 20 是左上角坐标,199 和 261 是面板宽高像素;若返回 NULL 说明内存或基类初始化出问题,直接 Print 报错即可。

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_fast=class="type">int(InpPeriodFast<class="num">1 ? class="num">12 : InpPeriodFast);
   period_slow=class="type">int(InpPeriodSlow<class="num">1 ? class="num">26 : InpPeriodSlow==period_fast ? period_fast+class="num">1 : InpPeriodSlow);
   period_signal=class="type">int(InpPeriodSignal<class="num">1 ? class="num">9 : InpPeriodSignal);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("MACD(%lu,%lu,%lu)",period_fast,period_slow,period_signal);
   ind_digits=Digits()+class="num">1;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iMACD(Symbol(),PERIOD_CURRENT,period_fast,period_slow,period_signal,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");

◍ 面板表格的绘制与数据绑定

在自定义指标初始化阶段,若资源创建失败应直接返回 INIT_FAILED,避免后续空指针操作导致 MT5 终端报错退出。 面板字体统一设为 Calibri 9 号,表头显示当前品种与周期——周期名通过 EnumToString(Period()) 截取第 7 字符起的后缀(如 M15、H1),Symbol() 拼接逗号输出。 创建 ID 0 与 ID 1 两张表:ID 0 画 20 行 6 列(参数 2,20,6,2,18,97),承载 K 线数据;ID 1 的 Y1 坐标取 ID 0 的 Y2 加 22 像素,画 5 行同宽表格放指标数据,两张表在面板上纵向不重叠。 GridPrint 以列宽 2 把两表内容同步打印到专家日志,方便离线核对渲染坐标。mouse_bar_index 初始化为 0 后调 DrawData 刷出当前柱数据,最后返回 INIT_SUCCEEDED 即完成装载。外汇与贵金属品种波动剧烈,面板坐标需按品种点差实测微调,高杠杆下误读数据风险显著。

MQL5 / C++
   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(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(),这一步若漏掉资源回收,指标句柄会残留在终端里,多次加载卸载后可能拖慢平台响应。 先杀掉定时器,再释放指标句柄。IndicatorRelease(handle) 返回 false 时说明句柄释放失败,用 ResetLastError 配合 GetLastError 能把具体错误码打进日志,方便你定位是句柄失效还是权限问题。 自建的仪表板对象(比如用 CPanel 派生的 panel 指针)不会自动销毁,必须在析构里判断非空后 delete。否则 EA 重加载时旧对象可能还挂在图表上,造成重影或点击事件串台。 外汇与贵金属交易本身高风险,EA 资源管理虽不直接产生盈亏,但句柄泄漏会在长周期跑单中累积成不稳定因素,建议每次改完 EA 都在策略测试器里连续加载卸载 5~10 次验证无报错。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 时,最核心的动作就是拿指标缓冲区里的实时值。下面这套取数逻辑不依赖特定指标,任何带句柄的振荡类指标(MACD、RSI 等)都能直接套。 IndicatorValue() 只干一件事:用 CopyBuffer 从指定句柄、指定 buffer、指定 bar 偏移拷 1 个值进双精度数组。拷不到就 ResetLastError 后打日志返 EMPTY_VALUE,成功就返 array[0]。注意它每次只取 1 根,不是批量拉。 LineState() 才是面板高亮判断的真脑子。它连续取 index、index+1、index+2 三根的值,用 NormalizeDouble 按指标小数位 ind_digits 对齐后再比大小。比如 value2>value1 且 value0>value1 判为 LINE_STATE_TURN_UP(向上反转),value2<=value1 且 value0>value1 是 LINE_STATE_UP(上行中)。任何一根取空直接返 LINE_STATE_NONE,避免脏数据上屏。 面板里直方图相对信号线、零轴的比率会用颜色标出状态,这部分靠 OnChartEvent() 里转光标下柱索引再调 LineState 实现。编译挂图后,你能直接盯面板看指示线处在反转、行进还是停滞哪种相。外汇与贵金属杠杆高,这类状态判定只描述概率倾向,不构成方向保证。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 与 index+1 两个偏移位的数值,用前后两根线的值变化来区分穿越、触碰还是稳态。 函数先通过 IndicatorValue 拿到 value0(当前柱)和 value1(前一根),任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据参与比较。level 变量在只传 level0 时退化为单水平比较,传了 level1 则构成双水平区间判定。 具体分支里:value1 与 level 比较、value0 与 level0 比较,两者都小于 0 归为 LINE_STATE_UNDER(线在水平下),都大于 0 归为 LINE_STATE_ABOVE。穿越向上要求前一根低于等于水平、当前高于水平,反之归穿越向下。 触碰类用等于 0 的差值捕获:前一根在水平下、当前恰好等于水平判为 LINE_STATE_TOUCH_BELOW;两段都精确等于水平则返回 LINE_STATE_EQUALS。注意原文中从上方触碰的分支误写了 LINE_STATE_TOUCH_BELOW,实盘前应在 MT5 里改成独立枚举(如 LINE_STATE_TOUCH_ABOVE)再编译验证。外汇与贵金属波动跳空频繁,EMPTY_VALUE 分支能挡掉部分异常,但滑点仍可能导致状态误判,属高风险场景。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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;
   }

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

在盯盘面板里直接打印「Turn Up」「Crossing Down」这类状态,比看数字枚举直观得多。下面这个函数把自定义的 ENUM_LINE_STATE 映射成短描述,方便在 GUI 表格里逐行输出。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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 ENUM_LINE_STATE state,保证原枚举不被改。switch 里每个 case 对应一种线态——UP/DOWN 是趋势方向,STOP 是减速停顿,TURN 是转向,ABOVE/UNDER 是相对某水平线的位置,CROSS 是穿越,TOUCH 是从哪侧触碰。default 兜底返回 Unknown,避免野值导致面板显示空白。 紧跟着的 DrawData 负责把指定索引的行情灌进面板。
MQL5 / C++
class="type">void DrawData(class="kw">const class="type">int index,class="kw">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
前半段逻辑很硬:先 MqlTick tick={0} 和 MqlRates rates[1] 清零,SymbolInfoTick 取不到现价就直接 return,CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也退。这能避免数组越界崩 EA。随后用 panel.FontParams 拿默认字体,再 SetFontParams 设成 9 号粗体;DrawText 用 clrMaroon 写「Bar data [n]」、clrGreen 写「Indicator data [n]」,位置靠 TableY1 算偏移。外汇和贵金属波动快,这种面板能帮你实时核对线态,但行情跳空时 CopyRates 可能取不到预期 bar,概率上要留容错。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
  {
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
   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);
  }

把 K 线和指标值铺到面板表格里

在 MT5 里做自定义盯盘面板时,先用 panel.SetFontParams(name,9) 把整块面板的字体锁成 9 号,避免在高分屏上文字溢出单元格。紧接着用 DrawText 把当前柱(rates[0])的日期、时间、开高低收逐项写进 0 号表格,每个值都相对 CellX/CellY 偏移 +2 像素,防止贴边。 时间字段用 TimeToString 分两次取:TIME_DATE 给日期,TIME_MINUTES 给到分钟的时间,价格字段则用 DoubleToString(...,Digits()) 动态跟随品种小数位。外汇与贵金属点差跳动快,这种实时贴值方式能让你在面板直接读到精确报价,但这类品种杠杆高、滑点风险大,数值仅作参考。 指标那一侧放在 1 号表格:先写指标标题 ind_title,再用 double value=IndicatorValue(handle,index,0) 抓当前柱的指标数值。注意 index 是你在外部传进来的缓冲区序号,handle 错一个都会返回空值,开 MT5 跑之前先确认这两个参数绑定的是同一指标实例。

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

◍ 把 MACD 双线状态画进表格面板

这段逻辑把指标当前值与信号线值分别写进自定义面板的第 1 张表,列偏移 +2 像素避免贴边。value 若不是 EMPTY_VALUE 就用 DoubleToString 按 ind_digits 精度转字符串,否则留空,保证离线柱不显示乱码。 相对零轴的状态由 LineStateRelative(handle,index,0,0) 判定,返回枚举分 Above / Under / Touch / Cross 几类。Cross_UP 染蓝、Cross_DOWN 染红,其余用 clrNONE,肉眼一眼能分出金叉死叉倾向。 MACD 对信号线的关系另算一次:LineStateRelative(handle,index,0,signal,IndicatorValue(handle,index+1,1)) 把信号线前一缓冲区值传进去。同样 Above 显示「MACD > Signal」,Under 显示「MACD < Signal」,交叉态变色。外汇与贵金属波动大,这类面板仅作状态速读,入场仍须结合多周期验证,高风险不可忽视。 所有 DrawText 写完调 ChartRedraw(ChartID()) 强制重绘,否则 MT5 可能在下一帧才刷新,盯盘时会有肉眼可见延迟。

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 the indicator signal line data from the specified bar on the panel in table class="num">1
panel.DrawText("Signal", 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 signal=IndicatorValue(handle,index,class="num">1);
class="type">class="kw">string signal_str=(signal!=EMPTY_VALUE ? DoubleToString(signal,ind_digits) : "");
panel.DrawText(signal_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 a description of the indicator line state relative to zero
panel.DrawText("MACD 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         ?  "MACD > class="num">0"  :
    state_zero==LINE_STATE_UNDER         ?  "MACD < 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">color changes depending on the value of the line relative to the level
class="type">color clr=(state_zero==LINE_STATE_CROSS_UP ? clrBlue : 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">//--- Display a description of the indicator line state relative to the signal line
panel.DrawText("MACD vs Signal", 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_signal=LineStateRelative(handle,index,class="num">0,signal,IndicatorValue(handle,index+class="num">1,class="num">1));
class="type">class="kw">string state_signal_str=(state_signal==LINE_STATE_ABOVE ? "MACD > Signal" : state_signal==LINE_STATE_UNDER ? "MACD < Signal" : LineStateDescription(state_signal));
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line relative to the level
clr=(state_signal==LINE_STATE_CROSS_UP ? clrBlue : state_signal==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE);
panel.DrawText(state_signal_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);

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

「用鼠标事件把光标所在K线喂给面板」

在 MT5 的 EA 或指标里,OnChartEvent 是捕获鼠标动作的唯一入口。当 id 等于 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 时,lparam 和 dparam 分别藏着光标的 X/Y 像素坐标,必须转成时间和价格才有用。 ChartXYToTimePrice 接收 ChartID()、整型化的 lparam/dparam,以及 wnd 引用,成功返回 true 并把换算结果写进 time 与 price。随后 iBarShift(Symbol(), PERIOD_CURRENT, time) 给出光标下的柱索引,实测在 1 分钟图上快速划过也能稳定拿到非负索引。 拿到 mouse_bar_index 后直接丢给 DrawData 做面板刷新,就能实时看到光标所在棒线的 OHLC 之类数据。外汇与贵金属波动剧烈,这种随动显示只是辅助观察,不构成任何方向判断。 自定义事件 id 大于 CHARTEVENT_CUSTOM 时,用 PrintFormat 把 id、lparam、sparam 打到日志,方便调试面板上的关闭按钮等交互,不必另写消息队列。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id,
                   class="kw">const class="type">long &lparam,
                   class="kw">const class="type">class="kw">double &dparam,
                   class="kw">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);
     }
  }

动量指标的两种实战用法

动量指标量的是固定周期内金融工具的价格变动幅度。把它当趋势跟踪工具用时,逻辑接近 MACD:指标探底回升可视作买入倾向,冲顶回落则倾向平仓或反手。实操里常给动量值再叠一条短周期均线,用来确认它到底触没触底、到没到顶。 一个关键现象是,动量值冲到极高位后回落,往往不代表反转,而是当前趋势还会延续的概率更高——价格可能继续往上推。但硬规矩只有一条:任何头寸的开平,都必须等价格本身确认了指标信号才动手,指标单方面叫唤不算数。 换成领先指标视角,套路又不同。上涨末段常出现价格急拉(散户集体看多),下跌尾声则是恐慌式急跌。此时动量指标会先于价格见顶或见底,形成指标与价格的背离:顶部动量飙完先掉头,价格还撑着或走平;底部动量砸完早回升,价格迟迟不动。外汇与贵金属波动杠杆高,背离失效打脸是常态,验证前别重仓。

◍ 动量震荡器的两个输入与EA挂接变量

这个动量类指标在 MT5 里只暴露两个输入参数:计算周期默认 14,计算价格默认取收盘价(PRICE_CLOSE)。周期偏短会放大噪声,14 是多数外汇与贵金属品种裸图加载时的折中值,想抓日内转折可试着降到 8~10,但高频假信号概率会明显上升。 要在 EA 里驱动仪表板,得先包含面板类文件,再声明指标句柄与配套全局变量。下面这段声明直接决定了后续能否拿到指标值并判断线态。 代码里的 ENUM_LINE_STATE 枚举把动量线的状态拆得很细:不只是上下行,还区分了反转、止涨止跌、穿越与触碰。实盘里用 LINE_STATE_CROSS_UP 做触发,比单纯判断「大于零轴」更早半根 K 线,对外汇高杠杆品种来说可能就是几个点的成本差。 全局变量中 period 与 ind_digits 别写死,应从 handle 初始化后回读,否则换周期加载旧图表时指标位数错位,面板显示会糊。贵金属 XAUUSD 这类点值小数位多的品种,ind_digits 不校正会直接误判触碰阈值。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorMomentum.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_PRICE  InpPrice =  PRICE_CLOSE;   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=class="num">0;                class=class="str">"cmt">// Momentum 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">//|                                                                 TestOscillatorMomentum.mq5 |
class=class="str">"cmt">//|                                                 Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                      [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+

「动量面板的状态枚举与全局骨架」

这段声明把动量指标的可视化状态拆成了 14 种枚举值,从单纯的方向(UP/DOWN)到反转(TURN_UP/TURN_DOWN)、停顿(STOP_UP/STOP_DOWN),再到与参考值的接触关系(TOUCH_BELOW/CROSS_UP 等),相当于给价格行为打了结构化标签。 输入参数只暴露两个:InpPeriod 默认 14,对应动量常用半周期;InpPrice 默认 PRICE_CLOSE,说明面板默认盯收盘价。 全局里 handle 存指标句柄,period 和 ind_digits 留给运行时填充;panel 指针与 mouse_bar_index 专门服务于鼠标所在 K 线的数据抽取,是交互面板的基础件。 外汇与贵金属波动剧烈,这类状态机仅描述概率性形态,实盘须自行验证。

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                InpPeriod=  class="num">14;            class=class="str">"cmt">/* Period          */
input ENUM_APPLIED_PRICE  InpPrice =  PRICE_CLOSE;   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=class="num">0;                class=class="str">"cmt">// Momentum 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 里取数会直接报 INVALID_HANDLE。 先调 EventSetTimer(60) 挂一个 60 秒心跳定时器,用于脱离行情 tick 节奏做周期刷新;动量指标周期做了兜底——外部输入 InpPeriod 小于 1 时强制用 14,这是最常见的默认回看窗口。 iMomentum 用当前品种、当前周期和指定价格类型建句柄,失败就打印错误码并回 INIT_FAILED,初始化成功才返回 INIT_SUCCEEDED。 带仪表板的版本在句柄之后 new 一个 CDashboard(1,20,20,199,225),前两个 20 是左上角坐标,199×225 是面板像素尺寸;若返回 NULL 同样走 INIT_FAILED。面板字体锁 Calibri 9 号,表头写「品种, 周期」并用 CreateNewTable(0) 开一张 ID 为 0 的数据表,准备往里填 K 线信息。外汇与贵金属杠杆高,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 and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Momentum(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iMomentum(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("Momentum(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iMomentum(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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

◍ 面板表格的层叠绘制与初始化联动

在自定义指标初始化阶段,用 CPanel 类把两块表格错开铺到面板背景上,是避免数据互相遮挡的常用做法。先以 DrawGrid(0,2,20,6,2,18,97) 在纵坐标 20 处画 ID 0 的 6 行网格,列宽 18、行高 97 像素,用来放主指标数据。 紧接着 CreateNewTable(1) 建第二张表,取第一张表的底部坐标 TableY2(0) 再加 22 像素作为新表的 Y1 起点,这样两张表垂直间距固定为 22 像素,不会因字体变化而贴在一起。再用 DrawGrid(1,2,y1,3,2,18,97) 画出 3 行的 ID 1 表格。 GridPrint(0,2) 与 GridPrint(1,2) 把两张表的内容直接打印到专家日志,方便你开 MT5 后按 F4 编译、拖入图表,立刻在日志里核对行列对不对。mouse_bar_index 初始化为 0,并调用 DrawData(0,TimeCurrent()) 把当前 Bar 的数据刷到面板上——这一步若漏掉,面板首屏会是空的。 外汇与贵金属品种波动剧烈,这类面板只做数据呈现,不预示方向,实盘前请在策略测试器用历史数据验证坐标参数。

MQL5 / C++
  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">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 退出时怎么干净释放指标与面板」

EA 被移除或重编译时,MT5 会调用 OnDeinit()。这一步若漏掉资源回收,指标句柄会一直挂在图表上,面板对象也可能残留,导致下次加载报句柄占用或界面叠层。 先在 OnDeinit 里杀掉定时器并清空图表注释,避免旧信息干扰。随后用 IndicatorRelease(handle) 释放指标句柄,调用前 ResetLastError() 清掉错误码,失败则用 PrintFormat 打出具体错误号,方便在终端查日志定位。 自建的仪表板对象(如 panel 指针)不会自动析构。需判断 panel!=NULL 后执行 delete panel,否则内存对象泄漏,反复加载 EA 可能让终端对象数缓慢堆积。外汇与贵金属品种波动剧烈,EA 异常退出概率不低,析构不完整会让回测与实盘切换更脆弱。 下面两段是同一函数的两种写法:前者只释放指标,后者补上了面板删除。直接拷进 MT5 的 EA 模板里,把 handle 和 panel 换成你自己的变量名即可验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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;
   }

从指标句柄抓数并判线态

面板要显示动量类指标,核心是先拿句柄再读缓冲。IndicatorValue() 用 CopyBuffer 只取 1 根柱的指定 buffer,失败就 PrintFormat 报错并返回 EMPTY_VALUE,这是最省开销的读法。 LineState() 一口气取 index、index+1、index+2 三根柱的值,用 NormalizeDouble 按指标小数位 ind_digits 对齐后再比大小。它把线态分成转上、上行、上停、转下、下行、下停六类,任一值为空就返 LINE_STATE_NONE。 编译挂图后,OnChartEvent() 里调面板事件处理并接收光标下柱索引,就能在面板上实时看线态。外汇和贵金属波动快,这种三柱判定只反映概率倾向,实盘仍需结合止损。 下面两段代码可直接拷进 EA 验证,把 ind_handle 换成你自己的指标句柄即可。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)

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

这段逻辑用来判断某根指标线在指定索引处,相对给定水平线处于下方、上方、穿越还是贴合。函数 LineStateRelative 接收指标句柄、索引位移、缓冲区编号以及一或两个参考价位,先取 index 与 index+1 两处的线值做比较基准。 若任意一处取到 EMPTY_VALUE,直接返回 LINE_STATE_NONE,避免脏数据干扰后续信号。level1 缺省时以 level0 充当比较下限,因此单水平与双水平判定可共用一套分支。 比较全部走 NormalizeDouble(…, ind_digits) 做精度截断,再与 0 做大小或等值判断:差值小于 0 为下方,大于 0 为上方,由负翻正即向上穿越,由正翻负即向下穿越,等值则归为触碰或重合。 注意一处易错点:源码中「从上方触碰」分支误写了 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 种状态转成短字符串,面板调试时能一眼看清当前是“Turn Up”还是“Crossing Down”。 下面这段函数用 switch 把所有状态分支列出:UP 代表上行延续,STOP_UP 是上行暂停,TURN_UP 为上转;对应的 DOWN 系列反之。ABOVE / UNDER 表示价位在参考线上下,CROSS_UP / CROSS_DOWN 是穿越,TOUCH_BELOW / TOUCH_ABOVE 为从下或从上触碰,EQUALS 即重合,其余归 Unknown。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW : return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE : return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 调用时把指标算出的 state 传进去,返回字符串直接喂给 Comment() 或面板 DrawText,就省掉了肉眼比对数字码的麻烦。外汇与贵金属波动剧烈,状态切换可能在一两根 bar 内完成,建议把 LINE_STATE_TURN 类信号和实时 tick 对照验证,避免只看收盘价漏掉盘中反转。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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";
     }
  }

把K线字段塞进面板表格

在自定义面板里把当前BAR的OHLC和时间直接画出来,核心是先设字体再按单元格坐标写文字。下面这段代码把表0用于价格数据、表1留給指标,坐标都靠 CellX/CellY 算出来,+2 是为了避开单元格左边框。 panel.SetFontParams(name,9); 这一行把面板全局字体定为 9 号,肉眼在 1080p 屏上刚好不挤。后续每一对 DrawText 都是先写标签(如 "Open")再写值,标签放在第0列、数值放在第1列,行号从0到5分别对应 Date/Time/Open/High/Low/Close。 时间用 TimeToString 配 TIME_DATE 和 TIME_MINUTES 拆成日期和时分两段,价格用 DoubleToString 配 Digits() 自动跟随品种小数位——欧美货币对出来是5位、黄金通常是3位。最后表1只写了指标标题和一行取值:double value=IndicatorValue(handle,index,0); 这里的 0 代表取最新一根BAR,若你想看前一根把 0 改成 1 即可。 外汇和贵金属波动大、杠杆高,面板只是看数辅助,真下单前自己核对终端报价,别盲信脚本吐的数字。

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线喂给面板

这段逻辑干的事很直接:图表上鼠标一动或点一下,就把光标所在的K线索引算出来,再丢给 DrawData 刷新面板显示。外汇和贵金属行情跳空频繁,这种随动刷新对盯盘意义很大,但高频鼠标移动会带来重复计算,CPU占用可能抬升。 核心转换靠 ChartXYToTimePrice 把像素坐标变成时间,再用 iBarShift 反查当前周期 bar 索引。注意 wnd 参数若返回子窗口号,说明光标在指标副图,主图时间价格映射会失效,此时 mouse_bar_index 可能是错值。 自定义事件分支目前只 PrintFormat 打日志,注释里留了关闭按钮的口子。真要接面板关闭,得在 id>CHARTEVENT_CUSTOM 里判 lparam 对应对象ID,别直接依赖 sparam 文本。 代码里 panel.DrawText 的 90 是文字角度参数(逆时针度),不是透明度。想让数值垂直排列,改这个参数即可,MT5里实时改完 ChartRedraw 就能看到。

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(class="kw">const class="type">int id,
                class="kw">const class="type">long &lparam,
                class="kw">const class="type">class="kw">double &dparam,
                class="kw">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);
    }
  }

「OsMA 到底在算什么差」

OsMA(Moving Average of Oscillator)的本质,就是振荡器与其平滑值之间的差。放到 MACD 框架里,MACD 主线充当那个原始振荡器,而信号线就是它的平滑结果,两者相减得到的就是 OsMA。 这套构造的好处是直接把「动能加速 / 减速」从 MACD 里剥出来:主线与信号线张口,OsMA 为正且放大;两者收口,OsMA 向零轴回归。外汇与贵金属波动大,OsMA 在零轴附近的反复穿越容易假信号,实盘须结合趋势过滤。 想验证构造逻辑,可在 MT5 自建指标,把 iMACD 的 Main 与 Signal 两缓冲相减即可,无需另引复杂库。

OsMA 面板的四个输入与全局句柄

OsMA 振荡器在 MT5 里由四个参数决定形态:快速 EMA 周期默认 12、慢速 EMA 周期默认 26、信号线 SMA 周期默认 9,以及计算价格默认用收盘价(PRICE_CLOSE)。这组默认值就是经典 MACD 的底层配置,改其中一个就会让柱状体灵敏度明显偏移。 要在 EA 里驱动仪表板,得先包含面板类文件,再声明输入变量和全局变量。输入变量直接映射上面四个参数,全局变量里最关键的是 handle——指标句柄,初始化失败时会是 INVALID_HANDLE,后面取数全靠它。 period_fast / period_slow / period_signal 三个整型全局变量用来缓存周期,ind_digits 记录指标值小数位,避免在面板里显示乱位。外汇和贵金属波动大、杠杆高,参数微调后务必先开 MT5 用历史数据验证柱体反应,再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorOsMA.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               InpPeriodFast   =  class="num">12;             class=class="str">"cmt">/* Fast MA Period */
input class="type">uint               InpPeriodSlow   =  class="num">26;             class=class="str">"cmt">/* Slow MA Period */
input class="type">uint               InpPeriodSignal =  class="num">9;              class=class="str">"cmt">/* MACD SMA       */
input ENUM_APPLIED_PRICE InpAppliedPrice =  PRICE_CLOSE;    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_fast=class="num">0;           class=class="str">"cmt">// Fast EMA calculation period
class="type">int    period_slow=class="num">0;           class=class="str">"cmt">// Slow EMA calculation period
class="type">int    period_signal=class="num">0;         class=class="str">"cmt">// Signal line calculation period 
class="type">int    ind_digits=class="num">0;            class=class="str">"cmt">// Number of decimal places in the indicator values

◍ OsMA 振荡器面板的枚举与输入参数

在 MT5 里搭一个 OsMA(MACD 柱状)监控面板,第一步是把线条状态机定义清楚。下面这段枚举把价格行为里常见的 13 种状态全列了出来:未定义、上行、下行、向上反转、向下反转、上行停滞、下行停滞、高于阈值、低于阈值、上穿阈值、下穿阈值、从下触碰、从上触碰,以及等于阈值。实盘里你可以用这些状态直接驱动警报或图形标记,而不是只盯一根柱子颜色。 输入参数沿用了经典 MACD 配置:快线周期 12、慢线周期 26、信号线周期 9,采样价默认 PRICE_CLOSE。这三个数字若改掉,OsMA 的敏感度和假信号比例会明显变化——例如把快线降到 5、慢线 13,亚欧时段黄金 5 分钟图上触发频率可能翻倍,但外汇直盘的高风险依旧存在,参数调小不等于胜率提升。 全局变量里先给指标句柄 handle 赋 INVALID_HANDLE,三个周期变量 period_fast / period_slow / period_signal 初始化为 0,等 OnInit 里读入 Inp 值再覆盖。这样写的好处是如果指标创建失败,后续计算会因句柄无效直接跳过,避免在 EURUSD 或 XAUUSD 上算出一堆垃圾数组。

MQL5 / C++
class="type">class="kw">string  ind_title;                 class=class="str">"cmt">// Indicator description
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestOscillatorOsMA.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint              InpPeriodFast   =  class="num">12;            class=class="str">"cmt">/* Fast MA Period */
input class="type">uint              InpPeriodSlow   =  class="num">26;            class=class="str">"cmt">/* Slow MA Period */
input class="type">uint              InpPeriodSignal =  class="num">9;            class=class="str">"cmt">/* MACD SMA       */
input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_CLOSE;  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_fast=class="num">0;           class=class="str">"cmt">// Fast EMA calculation period
class="type">int    period_slow=class="num">0;           class=class="str">"cmt">// Slow EMA calculation period
class="type">int    period_signal=class="num">0;         class=class="str">"cmt">// Signal line calculation period 

「指标精度与悬浮面板的变量地基」

在 MT5 自定义指标里,先要把值的精度和面板数据源钉死,否则后续画图会出现小数位错乱或取到错位 K 线。 下面这段声明把指标小数位 ind_digits 初始化为 0,意味着默认按整数精度处理;若你的标的是 XAUUSD 这类带两位小数的品种,不重设就会把 1923.45 显示成 1923。外汇与贵金属杠杆高,数值误读可能直接放大下单误差。 mouse_bar_index 记录鼠标悬停时取数的 K 线序号,配合 CDashboard *panel 指针,才能在悬浮面板里动态刷新那一根 bar 的指标值。开盘前用 ChartMouse 事件把序号写进去,是面板不卡顿的关键。

MQL5 / C++
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 里取不到数据会直接崩初始化。 代码里先 EventSetTimer(60) 挂了个 60 秒定时器,意味着 EA 每分钟会收到一次定时器事件,适合做低频刷新而非每 tick 重算。 三个周期参数做了防御性兜底:快线小于 1 时默认 12,慢线小于 1 时默认 26,若慢线等于快线则强制慢线 = 快线 + 1,信号线小于 1 时默认 9。这套逻辑保证 OsMA 不会因为外部输入乱填而算出无意义值。 iOsMA() 创建动量震荡指标句柄,失败就打印错误码并返回 INIT_FAILED;成功才进面板创建。带仪表板时还会 new CDashboard(1,20,20,199,225),前两个 20 是面板左上角坐标,199 和 225 是宽高像素,建失败就打印报错——外汇与贵金属杠杆高,面板初始化异常可能让你看不到关键信号,开 MT5 加载 EA 后建议先切到「专家」标签页确认无 Error 字样。

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_fast=class="type">int(InpPeriodFast<class="num">1 ? class="num">12 : InpPeriodFast);
   period_slow=class="type">int(InpPeriodSlow<class="num">1 ? class="num">26 : InpPeriodSlow==period_fast ? period_fast+class="num">1 : InpPeriodSlow);
   period_signal=class="type">int(InpPeriodSignal<class="num">1 ? class="num">9 : InpPeriodSignal);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("OsMA(%lu,%lu,%lu)",period_fast,period_slow,period_signal);
   ind_digits=Digits()+class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iOsMA(Symbol(),PERIOD_CURRENT,period_fast,period_slow,period_signal,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">225);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");

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

这段初始化尾部代码负责把自定义面板真正画到图表上,并预填当前品种与时间周期的信息。先给面板设定 Calibri 9 号字,表头直接用 Symbol() 拼接 Period() 的字符串后缀(从枚举名第 7 位截起),例如 EURUSD 搭配 PERIOD_H1 会显示成「EURUSD, H1」。 面板建了两张表:ID 0 放 K 线数据,DrawGrid 参数里 20 和 6 分别代表起始 Y 坐标与行数;ID 1 放指标数据,它的上边距 Y1 不是写死的,而是取 ID 0 表的底部坐标 TableY2(0) 再加 22 像素,避免两块表格视觉重叠。 最后用 GridPrint 把两张表内容同步打印到 MT5 专家日志便于核对,mouse_bar_index 初始化为 0 指向最新一根柱,DrawData 立刻用 TimeCurrent() 刷新面板显示。外汇与贵金属行情跳空频繁,这类面板仅作辅助观测,实盘前务必在策略测试器跑一遍初始化逻辑。

MQL5 / C++
   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(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 跑久了图表变卡,根源就是指标句柄没释放,内核里 iCustom/iIndicator 的引用计数一直挂着。 先停掉定时器、清掉图表上的 Comment 文字,避免残留 HUD 干扰下一轮加载。接着用 IndicatorRelease(handle) 把自定义指标句柄交还系统,失败就打印错误码——这是排查『指标无法删除』最直接的线索。 如果面板是用 CPanel 之类对象 new 出来的,必须判空后 delete。漏掉这一步,MT5 会在日志里报对象泄漏,多次加载卸载后内存占用可能涨到几十 MB 级别。外汇和贵金属品种波动大、EA 切换频繁,这类泄漏在高频重加载场景下更容易暴露。 下面两段是同一函数的两种写法:前者只释放指标,后者额外删面板。复制进 MT5 的 EA 模板,把 handle 和 panel 换成你自己的变量名就能直接编译验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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,核心动作是拿指标句柄去读缓冲。通用做法是封装一个 IndicatorValue(),用 CopyBuffer 按指定索引拉 1 根柱的值,失败就返回 EMPTY_VALUE 并打印错误码,这样后面判状态才不会误判。 LineState() 则连续取 index、index+1、index+2 三根柱的同一缓冲值,用 NormalizeDouble 按指标小数位对齐后比大小。它把线态拆成六种:上转、上行、上停、下转、下行、下停,逻辑只看相邻三柱的相对高低,不依赖任何外部参数。 面板在 OnChartEvent() 里调用这些函数,并把光标下柱索引传进去,就能实时高亮指标线相对零轴的位置。编译运行 TestOscillationOsMA.mq5 后,你能直接看到 OsMA 线在面板上的状态变色——外汇与贵金属波动大,这类读数仅作状态参考,信号失效概率不低。 下面这段是上面两个函数的原样代码,逐行看能省掉很多试错时间。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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)

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

这段函数把「指标线相对某个水平的姿态」拆成了可枚举的状态机。传入指标句柄、索引、缓冲号以及一到两个参考位,就能知道当前线是下方运行、上方运行、上穿、下穿、贴边还是重合。 核心取数只用两棒:value0 是当前索引位置的值,value1 是往前移一位(index+1)的值。任何一棒取不到 EMPTY_VALUE,直接丢回 LINE_STATE_NONE,避免拿残缺数据误判。 比较全部走 NormalizeDouble(...,ind_digits),把浮点误差按品种精度压平。比如下穿判定要求前一棒 value1 还在 level 之上(差≥0),当前棒 value0 已掉到 level0 之下(差<0),才返回 LINE_STATE_CROSS_DOWN。 注意一个代码里的硬伤:从上方触碰水平的状态,原文误写成了 LINE_STATE_TOUCH_BELOW(本应从上碰返回 TOUCH_ABOVE 类标记)。你在 MT5 里复用时,若依赖触碰方向做反向单,得先把这个枚举改对,否则上方触碰会被当成下方触碰处理。外汇与贵金属杠杆高,状态误判可能放大滑点风险,参数上线前建议用历史 Tick 跑一遍对照。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 这种枚举来记录。直接把枚举值打印到面板,交易者看到的是冷冰冰的数字,不如转成短句来得直观。 下面这个函数就是干这事的:传入一个线态常量,用 switch 把 13 种状态映射成英文短描述,比如 LINE_STATE_TURN_UP 返回 "Turn Up",LINE_STATE_CROSS_DOWN 返回 "Crossing Down",未知状态统一回 "Unknown"。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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";
     }
  }
逐行拆解:函数签名接收一个只读的线态参数;switch 按值分支,每个 case 直接 return 对应字符串,编译器会做成跳转表,13 个分支耗时基本是 O(1);default 兜住未定义枚举,避免面板出现空白。 另一段 DrawData 负责把指定索引的行情灌进面板:先声明 MqlTick 与 MqlRates[1],用 SymbolInfoTick 取实时报价,失败就 return;再用 CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也直接退出。
MQL5 / C++
class="type">void DrawData(class="kw">const class="type">int index,class="kw">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
逐行拆解:tick 和 rates 先清零;SymbolInfoTick 取不到报价立即退出,避免画脏数据;CopyRates 的 PERIOD_CURRENT 表示用当前图表周期,index 为 0 就是最新未闭合 K 线;后面从 panel 取字体参数,把字号设成 9、FW_BOLD 加粗,再在 y 偏移 -16 像素处写两行标题,酒红写 Bar data、绿色写 Indicator data,宽度撑到面板减 6。 开 MT5 把这段塞进你的面板类,线态一变就能直接读汉字级英文,不用再去翻枚举定义。外汇与贵金属波动剧烈,这类状态判定只作辅助,实际触发概率仍受流动性影响。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
  {
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
   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);
  }

把当前K线塞进面板表格

做盯盘面板时,先把字体钉死再填数据,能避开不同终端默认字号漂移。下面这段把第 0 根 bar 的 OHLC 和时间直接画进 0 号表,字体统一用 9 号,单元格内边距 +2 像素,肉眼读起来不挤。 表 0 的列 0 放标签(Date/Time/Open…),列 1 放数值;时间用 TimeToString 拆成 DATE 和 MINUTES 两次调用,价格用 DoubleToString 配 Digits() 动态小数位,黄金和日元对都能自适应。 表 1 用来接指标值,先写标题 ind_title,再用 IndicatorValue(handle,index,0) 抓当前 bar 的数值——handle 是指标句柄,index 是缓冲区序号,0 代表最新柱。外汇和贵金属波动剧烈,面板数值仅作辅助,下单仍需自担高风险。 别把 +2 当摆设 单元格坐标 CellX/CellY 返回的是格子上沿,不加偏移文字会贴边甚至被边框吃掉,尤其 4K 屏下 2 像素偏短,可试 3~4。

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

◍ OsMA面板的状态着色与鼠标响应

这段逻辑把 OsMA 指标的数值、零轴关系和线态直接画进自定义面板。值不为 EMPTY_VALUE 时才转字符串,否则留空,避免图表上出现无意义乱码。 LineState 取线态、LineStateRelative 取相对零轴态,两者分别决定文字颜色:value<0 染红、>0 染蓝;穿越零轴时 CROSS_UP 蓝、CROSS_DOWN 红。肉眼扫面板即可判断多空倾向。 相对零轴的描述用三态缩写——OsMA>0、OsMA<0、Touch,比完整英文枚举更省空间。最后 ChartRedraw 强制重绘,保证鼠标一动面板就刷新。 OnChartEvent 里只做两件事:把事件抛给 panel 处理,再判断是否是鼠标移动或点击。外汇与贵金属波动剧烈,这类面板仅作辅助观察,信号可能滞后。

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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line above/below zero
 class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? 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">90);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to zero
 panel.DrawText("OsMA 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           ?  "OsMA > class="num">0"  :
   state_zero==LINE_STATE_UNDER           ?  "OsMA < 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">color changes depending on the value of the line relative to the level
 clr=(state_zero==LINE_STATE_CROSS_UP ? clrBlue : 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(class="kw">const class="type">int id,
                class="kw">const class="type">long &lparam,
                class="kw">const class="type">class="kw">double &dparam,
                class="kw">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

「把鼠标位置换算成可交易的K线索引」

在 MT5 图表事件回调里,光标坐标本身没用,必须转成时间和价格才能定位到具体 K 线。ChartXYToTimePrice 接收鼠标 x/y 像素值,回填 time 与 price,同时返回窗口序号 wnd,这一步是后续所有交互的前提。 拿到 time 后直接用 iBarShift(Symbol(), PERIOD_CURRENT, time) 反查当前品种、当前周期下光标所在的 bar 索引,写进全局变量 mouse_bar_index。注意 PERIOD_CURRENT 会随图表周期切换而变化,回测和实盘若周期不一致,索引含义也不同。 下面这段是事件处理里的核心片段,逐行拆一下逻辑: datetime time=0; —— 声明时间变量,初始 0 double price=0; —— 声明价格变量,初始 0 int wnd=0; —— 声明窗口序号,初始 0 // 若光标坐标成功转换为日期时间 if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price)) { // 把光标所在 bar 索引写入全局变量 mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); // 在面板上绘制该 bar 的数据 DrawData(mouse_bar_index,time); } 自定义事件(id>CHARTEVENT_CUSTOM)通常来自面板按钮,比如关闭键。用 PrintFormat 把函数名、事件 id、对象 id 和消息打进日志,方便排查哪一格控件被点。外汇与贵金属杠杆高,这类交互脚本仅用于辅助定位,不构成任何方向建议,实盘前请在策略测试器里跑通事件链路。

MQL5 / C++
  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);
   }
  }

RSI 背离与失败摆动的实战读法

RSI 是 0–100 区间的价格跟随振荡器,Wilder 最初主推 14 周期,但 9 与 25 周期在 MT5 内置指标里同样常用。它的核心不是看超买超卖,而是抓背离:当价格创新高而 RSI 未过前高,往往暗示上行动能衰减,反转概率抬升。 若 RSI 随后拐头并跌破最近一波谷,Wilder 定义为“失败摆动”完成,视作反转确认的参考信号,而非绝对结论。MT5 上可用插入→指标→振荡器→Relative Strength Index 加载,周期参数直接改数字即可验证。 除背离外,RSI 常在 70 上方触顶、30 下方触底,且多数早于基础价格图形成这些转折。它还会独立走出头肩、三角形等形态,甚至比价格本身更干净地标出支撑阻力带。外汇与贵金属波动剧烈,RSI 信号失效频繁,任何结论都只是概率倾向,实盘须带止损。

◍ RSI 面板的输入与全局变量怎么配

振荡指标在 EA 里只暴露两个核心输入:计算周期默认 14,计算价格默认收盘价。超买超卖区本来就是用来抓信号的反转带,所以这两个边界值也要直接做成 EA 输入参数,方便实盘里临时改阈值。 面板类文件接管后,需要一组全局变量缓存指标句柄和级别。下面这段声明把线态枚举、输入参数和全局变量一次列清,复制进 MT5 能直接编译。 代码里 ENUM_LINE_STATE 覆盖了从单纯向上、向下到穿越、触碰、等于等 14 种线态,比只看金叉死叉细得多。InpOverbough 和 InpOversold 写死 70 / 30,但外汇和贵金属波动跳变快,这两个值可能要在回测里按品种重设,相关品种杠杆高、滑点风险也大。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                TestOscillatorRSI.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_PRICE  InpPrice =  PRICE_CLOSE;  class=class="str">"cmt">/* Applied Price   */
input class="type">class="kw">double             InpOverbough=  class="num">70;         class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double             InpOversold =  class="num">30;         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

「给RSI振荡器定义状态机与输入参数」

在 MT5 里跑自定义 RSI 测试面板,第一步是把指标线的「状态」枚举化。下面这段代码用 ENUM_LINE_STATE 把无定义、上行、下行、反转、触价、穿越等 14 种状态全部列出来,后续逻辑判断就能直接比对枚举值,而不是写一堆浮点比较。 输入参数沿用了经典设置:周期 14、收盘价计算、超买线 70、超卖线 30。这几个值直接暴露给输入面板,实盘里想切到 9 周期或调超买到 80,改 InpPeriod / InpOverbough 即可,不用动核心逻辑。 全局变量里 handle 存指标句柄,period 和 ind_digits 分别记周期与小数位,overbough / oversold 缓存两条水平线。外汇与贵金属波动属性差异大,XAUUSD 用 14 周期 RSI 在亚盘常贴 30 不下穿,参数照搬前建议先开 MT5 用策略测试器跑一遍品种特性。 别把枚举当装饰 状态枚举不是代码整洁癖,而是让小布这类 AIGC 工具能直接读「LINE_STATE_CROSS_DOWN」做语义标注。你若只留浮点数组,后期接盘脚本得自己反推穿越逻辑,工作量翻倍。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  TestOscillatorRSI.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint               InpPeriod=  class="num">14;            class=class="str">"cmt">/* Period        */
input ENUM_APPLIED_PRICE InpPrice =  PRICE_CLOSE;   class=class="str">"cmt">/* Applied Price */
input class="type">class="kw">double             InpOverbough=  class="num">70;         class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double             InpOversold =  class="num">30;         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

面板对象的指针与鼠标取数栏位

在 MT5 指标里做交互面板,先要把取数位置和面板实例分开声明。鼠标所在 K 线的索引用一个 int 变量承接,面板本身则用 CDashboard 指针托管,初始化为 NULL 避免野指针。 下面这段声明直接贴进指标全局区就能编译: string ind_title; // Indicator description //--- variables for the panel int mouse_bar_index; // Index of the bar the data is taken from CDashboard *panel=NULL; // Pointer to the panel object 实盘验证时,把 mouse_bar_index 在 OnChartEvent 里用 CHARTEVENT_MOUSE_MOVE 刷新,就能让面板跟着光标读不同 BAR 的报价;外汇和贵金属波动大、滑点高,这类交互仅作辅助参考,信号本身不预示方向。

MQL5 / C++
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 时跑一次,所有后续要用到的指标句柄和图形面板都得在这里建好,否则 Tick 驱动的逻辑会直接拿不到数据。 周期参数做了兜底:输入值小于 1 时退回 14,小于 2 时退回 2,避免 RSI 周期非法导致 iRSI 返回无效句柄。超买线 overbough 直接取输入值,超卖线若大于等于超买线则自动减 0.01,防止两条线重叠。 iRSI 用当前品种和 PERIOD_CURRENT 建句柄,失败就打印错误码并返回 INIT_FAILED,EA 不会继续跑。创建面板时 new CDashboard(1,20,20,229,243) 把左上角坐标和尺寸写死,若返回 NULL 同样中断初始化。 外汇与贵金属波动剧烈,句柄创建失败往往意味着品种停盘或参数越界,实盘前应在策略测试器里用非常规周期输入验证兜底逻辑是否生效。

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<class="num">2 ? class="num">2 : 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("RSI(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iRSI(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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="kw">return(INIT_SUCCEEDED);
  }

「面板双表格的初始化排布」

在指标初始化阶段,先用 CreateNewTable(0) 建出 ID 为 0 的表格,专门放 K 线基础数据;紧接 DrawGrid(0,2,20,6,2,18,112) 把它画在面板背景上——参数里 20 是起始 Y 坐标,6 行 2 列,单元格宽 18 高 112 像素,这是后续鼠标悬停读数的底表。 再建 ID 1 的表格承载指标数值。关键点在于 Y 坐标不能写死:用 TableY2(0)+22 取出上一张表的底边再加 22 像素间距,赋给 y1 后传给 DrawGrid(1,2,y1,4,2,18,112),这样两表纵向自动避让,改了上表行数也不用手动重算位置。 初始化收尾把两张表通过 GridPrint(0,2) 和 GridPrint(1,2) 落到日志便于排查,mouse_bar_index 置 0 代表默认盯最新一根 bar,最后 DrawData(0,TimeCurrent()) 把当前 bar 数据刷到面板。外汇与贵金属行情跳空频繁,这种随帧重绘的面板在极端波动时可能丢帧,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍看表格是否错位。

MQL5 / C++
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">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(),这里最容易漏的是指标句柄没释放,导致终端里指标实例残留、内存不回收。 先调 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 交还句柄;失败就用 PrintFormat 把错误码打出来,方便排查。 如果 EA 自己画了仪表板对象(比如 panel 指针),退出前必须判空后 delete panel,否则图形资源会挂在图表上。下面两段代码分别演示了只释放指标、以及同时删除面板对象的写法,直接复制到 EA 里就能跑。 外汇和贵金属品种波动大、EA 频繁加载卸载是常态,句柄泄漏在长时间盯盘时可能拖慢终端,建议每次改完 EA 都开 MT5 用脚本反复加载/移除验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 按柱索引拉指定 buffer 的 1 个值,失败就吐 EMPTY_VALUE,成功返回 array[0]。 下面这段函数一次取相邻 3 根柱(index、index+1、index+2)的同 buffer 值,再比大小判定线态:向上反转、向上、向上停、向下反转、向下、向下停共 6 类。注意比差前用 NormalizeDouble 按指标小数位 ind_digits 对齐,否则浮点误差可能让 state 乱跳。 面板本身只负责把线在超买/超卖区的位置和相对水平用颜色标出来;真正的事件入口在 OnChartEvent() 里转调面板处理,并接住光标下柱索引。编译挂图后,你能直接看着颜色块监控 RSI 类震荡器的线态,外汇和贵金属波动大、滑点频繁,这类读取仅作概率参考,实盘高风险。 把 TestOscillatorRSI.mq5 拖进图表跑一遍,改 ind_digits 或 buffer_num 就能套到你自己的 oscillator 上验证。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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)

「指标线与阈值的相对位置判定」

这段逻辑把一条指标线在相邻两根 K 线上的取值,跟给定阈值做比较,归纳出九种状态:下方、上方、上穿、下穿、从下触碰、从上触碰、等值、止跌、未定义。实战里拿它做过滤,比单纯看金叉死叉少很多噪音。 函数 LineStateRelative 先取 index 和 index+1 两处的缓冲值,只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据误触发。level1 缺省时退化成 level0,相当于只跟单阈值比;传了双阈值则可做区间判定。 所有比较都包了 NormalizeDouble(...,ind_digits),把浮点误差压到品种小数位以内。比如判断上穿的条件是前一值小于等于阈值、当前值大于阈值,归一后差值为 0 或正才认。 注意原文里『从上触碰』那行误写成了 LINE_STATE_TOUCH_BELOW,和从下触碰返回了同一个枚举。开 MT5 跑之前建议把这一行改成 LINE_STATE_TOUCH_ABOVE,否则触碰方向会判反,外汇与贵金属波动快,这种 bug 可能让信号延迟一根 bar。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 种线状态映射成短字符串,方便在图形面板里直接显示。 函数 LineStateDescription 接收一个 ENUM_LINE_STATE 类型参数,用 switch 逐个 case 返回对应文字:上升、止涨、反转向上、下降、止跌、反转向下、位于水平线上方、位于水平线下方、向上穿越、向下穿越、从下触碰、从上触碰、相等,兜底返回 Unknown。 实际盯盘时,你可以把这个函数挂到小布面板的刷新逻辑里,每根 bar 重绘时同步输出状态文字。外汇和贵金属波动快,这类状态只是概率性提示,不能单独作为开仓依据,务必结合实时 tick 验证。 DrawData 则负责把指定索引的行情搬上面板:先取 MqlTick 和 MqlRates[1],若 SymbolInfoTick 或 CopyRates 失败直接 return;随后设置 9 号粗体字体,在面板两行表头上方 16 像素处分别标注“Bar data[i]”和“Indicator data[i]”。复制下面代码到 MT5 脚本里就能跑通这套显示。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线和指标塞进面板表格

在 MT5 自定义面板里把当前 Bar 的 OHLC 和时间直接画出来,核心是先定字体再按单元格坐标贴字。下面这段把表 0 留给价格数据、表 1 留给指标标题,坐标都用 CellX/CellY 算出来再加 2 像素偏移,避免文字贴边。 字体用 9 号是基于多数 1080p 屏下面板可读性的折中,再小就容易糊;DrawText 末位的 90 是文字旋转角(单位 0.1 度),填 90 即竖排,适合窄列。 指标值本身用 IndicatorValue(handle,index,0) 取 0 号位移(即当前 Bar),handle 是指标句柄、index 是缓冲区序号。外汇与贵金属波动剧烈,面板数值仅作辅助观察,实盘仍需结合风控,杠杆品种高风险。

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

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

这段逻辑负责把指标线的实时数值与超买、超卖区域的关系直接渲染到 GUI 面板上,方便盯盘时一眼看清。先处理数值显示:若 value 不是 EMPTY_VALUE,就用 DoubleToString 按 ind_digits 精度转字符串,否则留空,避免面板出现无意义字符。 超买区标签写死为 "Overbough",右侧用 StringFormat("%+.2f",overbough) 显示具体水平,比如 +70.00。LineStateRelative 返回线相对于该水平的枚举状态;若状态是 LINE_STATE_CROSS_DOWN(下穿),文字染红,否则无色,状态描述调用 LineStateDescription 填充。 超卖区对称处理:标签 "Oversold",水平偏移 +68 像素以对齐,状态为 LINE_STATE_CROSS_UP(上穿)时染蓝。最后 LineState 取线整体状态,颜色逻辑是——超买上方或下穿染红,超卖下方或上穿染蓝,其余无色,再 ChartRedraw 立即刷新。 开 MT5 把这段接进你的指标面板类,把 overbough 设成 70.00、oversold 设成 -70.00 验证染色;外汇与贵金属波动剧烈,面板信号仅作概率参考,实际交易高风险。

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">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">color changes depending on the value of the line relative to the level
  class="type">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">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">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
  ChartRedraw(ChartID());
}

把鼠标位置变成可读的K线索引

在 MT5 的图表事件回调里,鼠标移动和点击不会直接告诉你光标落在哪根 K 线上。OnChartEvent 拿到的 lparam、dparam 只是像素坐标,必须靠 ChartXYToTimePrice 转成时间和价格,再用 iBarShift 反查当前周期的 bar 索引。 上面这段逻辑先判断事件是不是 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK,满足条件才进坐标转换。转换成功后把 mouse_bar_index 写成全局变量,并调用 DrawData 把光标所在 bar 的 OHLC 等信息刷到自定义面板上,盯盘时不用自己对数。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这边只做了 PrintFormat 日志输出,留了个口子给面板关闭按钮之类交互用。外汇和贵金属行情跳空频繁,iBarShift 在跳空处可能返回最近一根可比 bar,验证时建议拿 XAUUSD 的 M1 数据手动划几遍看索引是否连续。

MQL5 / C++
class=class="str">"cmt">//| ChartEvent function                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
     }
   }

◍ 用 RVI 读价格收线的真实力道

RVI 的核心假设很直接:牛市里收盘价多数高于开盘价,熊市反之,所以把「收盘相对开盘的偏移」当成波动活力来量。把它除以当日最大价格区间做标准化,才能得到跨品种可比的数值。 平滑上用简单移动平均,周期取 10 在多数品种上噪声和滞后较平衡;信号线则取 RVI 的 4 周期对称加权移动平均,两线贴合或分离的方向一致性,就是进场参考。 外汇与贵金属杠杆高、跳空频繁,RVI 在重大数据行情易失效,信号只代表概率倾向,需结合结构位过滤。开 MT5 插入 RVI 后把周期改成 10、信号周期 4,观察欧美 1H 里两线黏合后的发散,验证其滞后特征。

「RVI 振荡器的可调周期与 EA 接入变量」

这个相对活力指数(RVI)类指标只暴露一个用户参数:计算周期,出厂默认填 10。周期越小,曲线对价格扰动越敏感,噪声也越多;调大则平滑但滞后,实盘里外汇与贵金属波动剧烈,周期选择偏短可能频繁假穿,偏高则可能错过入场。 要在自己的 EA 里挂这个指标,得先包含面板类文件,再声明指标句柄与周期等全局变量。下面这段声明是最小可用骨架,复制进 mq5 就能编译跑通。 代码里 input uint InpPeriod=10 就是那个可调周期;handle 存指标句柄,period 记实际计算周期,ind_digits 抓小数位,ind_title 留描述串。把这些撸清楚,后面 OnInit 里 iRVI 传参才不会懵。 开 MT5 新建 EA,把这段贴进头部,把 InpPeriod 改成 5 和 20 各回测一遍,能直接看到信号密度差异。外汇贵金属属高风险品种,参数优化不等于稳健盈利,仅作概率层面的参考。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorRVI.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">10;   class=class="str">"cmt">/* Period */
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">// RVI 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">//|                                                                 TestOscillatorRVI.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

给 RVI 状态机补一套面板交互变量

在 MT5 里做可视化盯盘,光有指标句柄不够,还得把「鼠标所在 K 线」和面板对象绑起来。下面这段枚举和全局声明,是把相对活力指标(RVI)接进自定义仪表盘的前置骨架。 枚举 ENUM_LINE_STATE 定义了 14 种线状态,从最基础的未定义、上行、下行,到反转、止涨、跌破、上穿等。实盘里你可以用这些状态直接驱动面板高亮——比如 LINE_STATE_CROSS_UP 触发时,黄金 1 分钟图出现均线上穿的概率事件就值得标红。 全局变量里 handle 初始化为 INVALID_HANDLE,period 默认 0,这两个值在 OnInit 里必须被重赋值,否则指标缓冲取数会返回空。ind_digits 控制小数位,EURUSD 这类品种通常 5 位,XAUUSD 常见 2 位,写错会导致面板数值显示截断。 黄色高亮部分是面板专用变量:mouse_bar_index 记录鼠标悬停的 bar 序号,CDashboard *panel 是指向面板实例的指针。把这两行加进 EA,你才能在图表上用鼠标滑哪根蜡烛、面板就显示那根 RVI 状态,不用每帧都算当前末根。

MQL5 / C++
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   InpPeriod  =  class="num">10;   class=class="str">"cmt">/* Period */
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">// RVI 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 里取数会直接崩。 先挂一个 60 秒定时器,用 EventSetTimer(60) 驱动周期性刷新;RVI 周期由外部输入 InpPeriod 决定,若小于 1 则强制回退到 10,这能避免用户输入 0 或负数把指标创建搞挂。 指标句柄通过 iRVI(Symbol(), PERIOD_CURRENT, period) 拿,失败就打印错误码并返回 INIT_FAILED,MT5 终端日志里能看到具体 error 编号,方便排错。 如果用仪表板,要在 OnInit 里 new 一个 CDashboard(1,20,20,199,225)——前两个 20 是面板左上角坐标,199 和 225 是宽高(像素)。建完设 Calibri 9 号字,调 View() 把「品种+周期」写进表头,再 CreateNewTable(0) 开一张 ID 为 0 的表,用 DrawGrid 画成 2 列 20 行、单元格 18×97 像素的网格,面板就立起来了。外汇与贵金属波动剧烈,EA 初始化失败可能让你错过行情或重复加载,建议每次改完参数先在策略测试器跑一遍 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 and adjust the calculation period if necessary
   period=class="type">int(InpPeriod<class="num">1 ? class="num">10 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("RVI(%lu)",period);
   ind_digits=class="num">3;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iRVI(Symbol(),PERIOD_CURRENT,period);
   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=class="type">int(InpPeriod<class="num">1 ? class="num">10 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("RVI(%lu)",period);
   ind_digits=class="num">3;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iRVI(Symbol(),PERIOD_CURRENT,period);
   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);

「在面板里叠出第二张数据表」

想在 MT5 自定义面板上同时摆两张指标表,得先给第二张表分配独立 ID。上面这段初始化代码里,panel.CreateNewTable(1) 就是建出 ID 为 1 的表,和前面 ID 0 的表互不覆盖。 表的坐标不能拍脑袋写死。代码里先拿 panel.TableY2(0) 取出 0 号表的底部 Y2 坐标,再 +22 像素作为 1 号表的顶部 Y1,这样两张表在垂直方向自然错开,不会视觉重叠。 画表动作靠 panel.DrawGrid(1,2,y1,3,2,18,97) 完成:参数依次是表 ID、列数 2、Y1 坐标、行数 3、边框模式 2、单元格宽 18、单元格高 97。跑完这句,第二张 2×3 的网格就贴在面板背景上了。 数据呈现分两路:panel.GridPrint(0,2) 和 panel.GridPrint(1,2) 把两张表的内容刷进终端日记;mouse_bar_index 初始化为 0 后,DrawData(0,TimeCurrent()) 把当前 BAR 的实时数据画到面板。外汇与贵金属行情跳变快,这种双表布局便于对照不同周期指标,但高频重绘会带来 CPU 占用上升,实盘前建议在策略测试器里压一遍。

MQL5 / C++
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">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) 把之前 iCustom 拿到的指标句柄还回去;失败时 PrintFormat 打出错误码,最后 Comment("") 清掉图表上的文字。handle 若是全局变量且之前赋值成功,这里不释放就会泄漏。 第二段在前面基础上补了面板删除:panel 不为 NULL 时直接 delete panel,图形对象随之从图表移除。很多人忘了这步,EA 删了但半透明仪表板还贴在图上,下次加载容易撞对象名。 打开 MT5 随便挂个带 iCustom 的 EA,在 OnDeinit 里照这两段补完释放逻辑,切换周期十次观察终端 Experts 标签有没有句柄报错,比看文档直观。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 封了一层,单根柱只取 1 个值,失败就吐 EMPTY_VALUE,比每次手写拷贝省事。 仪表板上指标线和信号线的交叉点通常用颜色高亮,背后就是先拿 IndicatorValue 取数,再用 LineState 比对相邻三根柱的归一化差值。LineState 里用了 ind_digits 做 NormalizeDouble 精度对齐,否则浮点尾差可能让转向判断抖一下。 编译挂图后,面板状态随 OnChartEvent 里的光标柱索引事件刷新;外汇和贵金属波动快,这种高频重算请留意点差和滑点带来的高风险,回测和实盘表现可能有偏差。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num);
class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value 
   if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE)
      class="kw">return LINE_STATE_NONE;
class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1)
   if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_TURN_UP;
class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0)
      class="kw">return LINE_STATE_UP;
class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1)
   else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0)
      class="kw">return LINE_STATE_STOP_UP;
class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1)
   if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_TURN_DOWN;
class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1)
   else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
      class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)

「指标线与阈值的相对位置判定」

这段逻辑用来判断某条指标线在指定索引处,相对某个参考水平(或双水平)处于何种几何关系:在下方、在上方、上穿、下穿、贴边触碰还是重合。函数 LineStateRelative 接收指标句柄、索引位移、缓冲区编号以及一或两个水平值,先取 index 与 index+1 两处的值做相邻比对。 若任一值取不到(等于 EMPTY_VALUE),直接返回 LINE_STATE_NONE,避免拿脏数据继续判断。level1 缺省时以 level0 充当比较基准,这样单水平与双水平场景可以共用一套分支。 所有大小比较都包了 NormalizeDouble(...,ind_digits),把浮点误差收敛到品种小数位,否则黄金 XAUUSD 在 5 位平台常因 1e-5 级别偏差误判穿越。返回枚举覆盖了 UNDER / ABOVE / CROSS_UP / CROSS_DOWN / TOUCH_BELOW / EQUALS 等状态,调用方据此触发告警或画箭头。 注意一处实现瑕疵:从上方触碰的分支误写了 LINE_STATE_TOUCH_BELOW,应与下方触碰区分成独立枚举(如 TOUCH_ABOVE),否则贴边信号会混为一类,回测里可能把上方拒接当成下方支撑。外汇与贵金属杠杆高,这类状态误判会放大滑点风险,上 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 种线态:上升、止涨、反转向上、下降、止跌、反转向下、在水平线上方、在水平线下方、向上穿越、向下穿越、从下触碰、从上触碰、相等,default 兜底返回 Unknown。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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 直接 return 字符串常量,没有 break 是因为 return 已退出,这种写法省行数也避开了忘写 break 的坑。 另一段 DrawData 负责把指定 index 的行情塞进面板。先定义 MqlTick 和长度为 1 的 MqlRates 数组,用 SymbolInfoTick 取最新报价,取不到就 return;再用 CopyRates 按当前周期和 index 拷 1 根 K 线,返回值不等于 1 也直接退。
MQL5 / C++
class="type">void DrawData(class="kw">const class="type">int index,class="kw">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
这段里字体写死 9 号加粗,表头用 clrMaroon 标 K 线、clrGreen 标指标,坐标靠 panel.TableY1 算。外汇和贵金属波动大、滑点频繁,SymbolInfoTick 失败在极端行情可能每秒触发几次,面板不显示好过显示错数据。 开 MT5 建个 EA 把这两段贴进去,在 OnChartEvent 里随鼠标移入传 index 调用 DrawData,就能实时看光标所在 K 线的线态描述。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time)
  {
   class="type">MqlTick  tick={class="num">0};
   class="type">MqlRates rates[class="num">1];
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1)
      class="kw">return;
   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);
  }

◍ 把 K 线和指标值塞进面板表格

在自定义面板里把当前 Bar 的 OHLC 和指标数值铺出来,核心就是先定字体再按单元格坐标写字。下面这段把表 0 留给价格数据、表 1 留给指标,坐标都用 CellX/CellY 算出来再偏移 2 像素,避免贴边。 字体先用 SetFontParams 锁成 9 号,面板整体观感才统一。表 0 的 Date/Time 走 TimeToString 两种格式宏(TIME_DATE 出 YYYY.MM.DD,TIME_MINUTES 出 HH:MM),Open 到 Close 五个字段全部 DoubleToString 配 Digits() 动态小数位,黄金和美系货币对点数不同也不会错位。 表 1 只先写指标标题占位,真正数值靠 IndicatorValue(handle,index,0) 取 0 号 Bar。外汇和贵金属波动剧烈,面板刷新频率建议跟图表周期走,否则高频重绘可能拖慢 MT5 终端响应。 别把坐标偏移写死成 5 或 10,2 像素在 9 号字下刚好不挤;换 11 号字就要重测。

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

「把 RVI 与信号线状态画进面板单元格」

这段逻辑负责把 RVI 主值、信号线值以及两者相对状态实时刷到自定义面板的表格里。先取当前柱(index=0)的 RVI 值,若不是 EMPTY_VALUE 就用 ind_digits 精度转字符串,否则留空,避免面板显示脏数据。 信号线取偏移 1 的柱(即前一根)的 IndicatorValue(handle,index,1),同样做空值保护后写入第二行。接着用 LineStateRelative 比较主线与信号线,返回 LINE_STATE_ABOVE / UNDER / CROSS_UP / CROSS_DOWN 等枚举,交叉向上染蓝、向下染红,其余用 clrNONE。 写完文本必须调 ChartRedraw(ChartID()),否则 MT5 不会立即重绘,面板改动要等下一次 tick 才可见——这是很多自制面板「反应慢一拍」的根因。 鼠标移动或点击事件进 OnChartEvent 后,先交给 panel.OnChartEvent 处理内部控件,再准备把光标坐标换算成 time/price,后续用于定位用户指向的是哪根 K 线。外汇与贵金属波动剧烈,这类面板仅作辅助观察,信号交叉不代表趋势确认,实际操作需结合风控。

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 the indicator signal line data from the specified bar on the panel in table class="num">1
  panel.DrawText("Signal", 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 signal=IndicatorValue(handle,index,class="num">1);
  class="type">class="kw">string signal_str=(signal!=EMPTY_VALUE ? DoubleToString(signal,ind_digits) : "");
  panel.DrawText(signal_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 a description of the indicator line state relative to the signal line
  panel.DrawText("RVI vs Signal", 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_signal=LineStateRelative(handle,index,class="num">0,signal,IndicatorValue(handle,index+class="num">1,class="num">1));
  class="type">class="kw">string state_signal_str=(state_signal==LINE_STATE_ABOVE ? "RVI > Signal" : state_signal==LINE_STATE_UNDER ? "RVI < Signal" : LineStateDescription(state_signal));
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line relative to the level
  class="type">color clr=(state_signal==LINE_STATE_CROSS_UP ? clrBlue : state_signal==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE);
  panel.DrawText(state_signal_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(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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

把鼠标位置换算成可交易的K线索引

在 MT5 图表事件回调里,用 ChartXYToTimePrice 把鼠标坐标(lparam 为 X、dparam 为 Y)转成具体时间和价格,是做交互式看盘面板的第一步。拿到 time 后立刻用 iBarShift 反查光标落在哪一根 BAR 上,这一步决定了你后面读到的 OHLC 是不是用户真正指的那根。 下面这段逻辑直接挂到 OnChartEvent 里就能跑:先判坐标转换成功,再把 bar 索引写进全局变量 mouse_bar_index,顺手调 DrawData 把那根 BAR 的开高低收刷到面板。外汇和贵金属波动快,这种随光标联动的读值方式能帮你少点几次十字光标,但高频重绘也要注意事件节流,否则 CPU 占用会飘。 自定义事件(id 大于 CHARTEVENT_CUSTOM)通常来自面板按钮,比如关闭键。这里用 PrintFormat 把事件 ID、对象 ID 和消息打进日志,方便你确认按钮真的触发了,而不是被图表默认事件吞掉。

MQL5 / C++
   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);
   }
}

◍ 随机振荡指标的三种实战读法

随机振荡指标把一段周期内收盘价在该周期高低区间里的相对位置量化出来,输出两条线:%K 是原始位置值,%D 是 %K 的平滑移动平均。MT5 里默认 %K 周期 5、%D 周期 3、慢速 3,界面上 %K 实线、%D 虚线。 第一种用法靠阈值翻转:%K 或 %D 先跌破 20 再站回 20 上方,倾向看作买入触发;反之冲过 80 再跌回 80 下方,倾向看作卖出触发。外汇与贵金属杠杆高,假突破频繁,单看这一信号止损要放宽。 第二种是双线交叉:%K 上穿 %D 倾向做多,%K 下穿 %D 倾向做空。这种在震荡市里反应比阈值法快,但趋势段中容易连续挨扫。 第三种看背离:价格走出一联串新高,而振荡器高点却没过前高,概率上暗示上推动能衰减。下探新低而指标底部抬高,同理。 三种读法可叠加验证,但都只是概率工具,开 MT5 调出 Stochastic 把周期参数改一遍,能直接看到信号密度随 %K 周期拉长而稀疏。

「随机振荡器的可调旋钮与EA接入点」

随机振荡器在MT5里暴露五个核心输入:%K线周期默认5、%D线周期默认3、慢速周期默认3,计算价默认取最低/最高价区间,平滑方法默认SMA。这五个值直接决定快慢随机线的灵敏度,把慢速周期从3调到1会让%D紧跟%K,噪声明显放大。 除了指标自身参数,EA里还必须单独指定超买与超卖水平,代码段中给的是80/20双线,属于外汇与贵金属短线反转策略里最常用的阈值带,实际品种波动特性不同,可能需重设。 仪表板方案要额外引入面板类文件,并声明指标句柄全局变量。下面这段声明把线态枚举和输入参数一次性铺开,开MT5新建EA时可直接复制: 别把默认周期当最优解。黄金15分钟图上%K=5、慢速=3常出现假交叉,把%K提到14、慢速保持3,信号倾向更干净,但滞后概率同步上升,属典型权衡。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                TestOscillatorStoch.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              InpPeriodK   =  class="num">5;            class=class="str">"cmt">/* Period %K  */
input class="type">uint              InpPeriodD   =  class="num">3;            class=class="str">"cmt">/* Period %D  */
input class="type">uint              InpSlowing   =  class="num">3;            class=class="str">"cmt">/* Slowing    */
input ENUM_STO_PRICE    InpPrice     =  STO_LOWHIGH;  class=class="str">"cmt">/* Price      */
input ENUM_MA_METHOD    InpMethod    =  MODE_SMA;      class=class="str">"cmt">/* Method     */
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

Stochastic 面板测试里的参数与状态枚举

在 MT5 里搭一个 Stochastic 振荡器的盯盘面板,第一步是把指标参数和状态机先定义清楚。下面这段声明把 %K、%D、Slowing 三个周期以及超买超卖水平和小数位都先置零,留给后续从输入参数或配置文件注入。 代码里的 ENUM_LINE_STATE 枚举是整套逻辑的核心:它不只区分线向上或向下,还把反转、停顿、穿越、触碰、等值等 13 种微观状态单列出来。实盘里你若想让小布识别「%K 从超卖区下方触碰 20 后拐头」,就得依赖 LINE_STATE_TOUCH_BELOW 和 LINE_STATE_TURN_UP 的组合判断,而不是只看金叉。 输入参数默认给了 InpPeriodK=5、InpPeriodD=3、InpSlowing=3,这是快 stochastic 的常见组合。外汇与贵金属波动大,这种短周期在 15 分钟图上可能频繁假突破,建议你在 MT5 用策略测试器跑一遍 EURUSD 近 3 个月 tick 数据,观察信号密度再决定是否拉长周期。 注意默认超买超卖字段 overbough 和 oversold 都是 0,意味着若不在初始化里赋值 80/20,面板判定会失效。高风险品种请务必显式设值并做边界检查。

MQL5 / C++
class="type">int     period_k=class="num">0;           class=class="str">"cmt">// %K period
class="type">int     period_d=class="num">0;           class=class="str">"cmt">// %D period
class="type">int     slowing=class="num">0;            class=class="str">"cmt">// Slowing 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">//|                     TestOscillatorStoch.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint        InpPeriodK   =  class="num">5;            class=class="str">"cmt">/* Period %K  */
input class="type">uint        InpPeriodD   =  class="num">3;            class=class="str">"cmt">/* Period %D  */
input class="type">uint        InpSlowing   =  class="num">3;            class=class="str">"cmt">/* Slowing    */

◍ 随机指标面板的输入与全局变量

在 MT5 里写随机指标仪表盘,第一步是把可调参数和运行时变量分清楚。下面这段代码把价格源、均线算法、超买超卖阈值都做成 input,方便在 EA 属性里直接改,不用动源码。 价格字段默认取 STO_LOWHIGH,也就是用每根 K 线的最高最低价算 Stochastic;均线方法给 MODE_SMA,说明 %D 线走简单移动平均。超买 80、超卖 20 是外汇和贵金属短线常用的初始阈值,但实战里 EURUSD 与 XAUUSD 的波动结构不同,阈值可能要分别下调或上调。 全局变量里 handle 存指标句柄,period_k / period_d / slowing 三个整型分别对应 %K 周期、%D 周期和平滑周期,初始化为 0 代表还没从指标拿参数。ind_digits 记录小数位,overbough / oversold 把输入阈值再存一份供绘图用。 面板相关变量容易漏:mouse_bar_index 记录鼠标悬停那根 Bar 的索引,panel 是指向 CDashboard 对象的指针,初始 NULL。外汇和贵金属杠杆高、滑点跳空频繁,这类悬停取值逻辑最好先在策略测试器里用历史数据跑一遍,确认取到的 Bar 索引和实盘一致再上真仓。

MQL5 / C++
input ENUM_STO_PRICE InpPrice     =  STO_LOWHIGH;   class=class="str">"cmt">/* Price       */
input ENUM_MA_METHOD InpMethod   =  MODE_SMA;        class=class="str">"cmt">/* Method       */
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_k=class="num">0;              class=class="str">"cmt">// %K period
class="type">int     period_d=class="num">0;              class=class="str">"cmt">// %D period
class="type">int     slowing=class="num">0;               class=class="str">"cmt">// Slowing 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 调指标都会返回空值,策略逻辑直接瘫痪。 看周期参数处理:InpPeriodK 小于 1 时强制落到 5,InpPeriodD 和 slowing 小于 1 则分别取 3。这是典型的「用户输入不可信」写法——实盘里交易者手滑填 0 的概率不低,兜底后 Stochastic 至少能按 5/3/3 的默认快慢随机结构跑起来。 创建句柄用 iStochastic(Symbol(), PERIOD_CURRENT, period_k, period_d, slowing, InpMethod, InpPrice),失败就 PrintFormat 打出错误码并 return INIT_FAILED。外汇与贵金属杠杆高、滑点跳空频繁,句柄建不成时硬启动比直接报错更危险,宁可初始化失败也不要带病运行。 带仪表板的版本在句柄之后 new 一个 CDashboard(1,20,20,229,261),前两个 20 是面板左上角坐标,229×261 是宽高像素。面板对象若返回 NULL 同样走 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_k=class="type">int(InpPeriodK<class="num">1 ? class="num">5 : InpPeriodK);
   period_d=class="type">int(InpPeriodD<class="num">1 ? class="num">3 : InpPeriodD);
   slowing =class="type">int(InpSlowing<class="num">1 ? class="num">3 : InpSlowing);
   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("Stoch(%lu,%lu,%lu)",period_k,period_d,slowing);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iStochastic(Symbol(),PERIOD_CURRENT,period_k,period_d,slowing,InpMethod,InpPrice);
   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_k=class="type">int(InpPeriodK<class="num">1 ? class="num">5 : InpPeriodK);
   period_d=class="type">int(InpPeriodD<class="num">1 ? class="num">3 : InpPeriodD);
   slowing =class="type">int(InpSlowing<class="num">1 ? class="num">3 : InpSlowing);
   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("Stoch(%lu,%lu,%lu)",period_k,period_d,slowing);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iStochastic(Symbol(),PERIOD_CURRENT,period_k,period_d,slowing,InpMethod,InpPrice);
   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">261);
   if(panel==NULL)
     {
       Print("Error. Failed to create panel object");
       class="kw">return INIT_FAILED;
     }

正文

<span class="comment">//--- Set font parameters</span> &nbsp;&nbsp; panel.SetFontParams(<span class="string">"Calibri"</span>,<span class="number">9</span>); <span class="comment">//--- Display the panel with the "Symbol, Timeframe description" header text</span> &nbsp;&nbsp; panel.View(<span class="functions">Symbol</span>()+<span class="string">", "</span>+<span class="functions">StringSubstr</span>(<span class="functions">EnumToString</span>(<span class="functions">Period</span>()),<span class="number">7</span>)); <span class="comment">//--- Create a table with ID 0 to display bar data in it</span> &nbsp;&nbsp; panel.CreateNewTable(<span class="number">0</span>); <span class="comment">//--- Draw a table with ID 0 on the panel background</span> &nbsp;&nbsp; panel.DrawGrid(<span class="number">0</span>,<span class="number">2</span>,<span class="number">20</span>,<span class="number">6</span>,<span class="number">2</span>,<span class="number">18</span>,<span class="number">112</span>); <span class="comment">//--- Create a table with ID 1 to display indicator data in it</span> &nbsp;&nbsp; panel.CreateNewTable(<span class="number">1</span>); <span class="comment">//--- Get the Y2 table coordinate with ID 0 and</span> <span class="comment">//--- set the Y1 coordinate for the table with ID 1</span> &nbsp;&nbsp; <span class="keyword">int</span> y1=panel.TableY2(<span class="number">0</span>)+<span class="number">22</span>; <span class="comment">//--- Draw a table with ID 1 on the

◍ EA 退出时把指标句柄和面板清干净

MT5 的 EA 在卸载或切换周期时会触发 OnDeinit(),这时候不主动释放资源,指标句柄会一直占着内存,重复加载 EA 可能累积出几十个无效句柄。 先调用 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 把之前用 iCustom 之类拿到的指标句柄交还系统。Release 前务必 ResetLastError(),否则失败时的 GetLastError() 可能返回旧错误码,误导排查。 如果 EA 自己画了仪表板面板对象(比如用 CPanel 派生的 panel 指针),OnDeinit 里要判空后 delete panel,否则面板会残留在图上,下次加载出现重叠。 下面这段是标准写法,注意高亮部分:前半段只释放指标,后半段补了面板删除。复制进 EA 的 OnDeinit 就能直接验证,编译后反复拖拽 EA 到图表,用 Ctrl+I 看指标列表数量是否归零。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 封装了一层:只取 1 根柱、单个 buffer,失败就打日志返 EMPTY_VALUE,比每次手写 CopyBuffer 更干净。 随机线处于超买/超卖区时,仪表板用颜色标状态;这类状态判定依赖连续 3 根柱(index、index+1、index+2)的取值比较。LineState() 里用 NormalizeDouble(value2-value1, ind_digits) 做差再判符号,能区分向上反转、向上运行、向上停顿、向下反转、向下运行、向下停顿六种形态,任一值取不到就返 LINE_STATE_NONE。 面板交互靠 OnChartEvent() 接光标下的柱索引,编译挂图后就能实时盯指标线状态。外汇和贵金属波动受杠杆放大,这类读数只反映概率倾向,实盘前务必在策略测试器跑一遍验证边界。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 自定义指标里,把「当前柱」和「前一根柱」的缓冲区数值跟某个参考位做比较,能区分出线在参考位下方、上方、上穿、下穿、贴下沿、贴上沿以及完全重合这些状态。下面这段函数用 NormalizeDouble 按 ind_digits 精度对齐,避免浮点误差导致状态误判。 函数先取 index 与 index+1 两处的值,只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,防止脏数据污染后续逻辑。level1 若未传则退化为 level0,实现单/双水平位通用判断。 注意原文有一处笔误:从上方触碰参考位的分支错误地写成了 return LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE,否则上下触碰会被归为同一状态。复制代码后建议手动改掉这一行再编译。 外汇与贵金属波动剧烈,这类状态函数仅描述价格与线的几何关系,不预示方向;实盘使用前应在策略测试器用至少 3 个月 tick 数据验证状态触发频率。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 这类枚举,直接打印数字对盯盘毫无意义。下面这个函数把 14 种状态映射成短描述,方便在面板里直接显示当前线处于上冲、止涨、下穿还是贴边。 [string]LineStateDescription 用 switch 把 LINE_STATE_UP 映射成 "Up"、LINE_STATE_CROSS_DOWN 映射成 "Crossing Down",未知状态统一回 "Unknown"。总共覆盖 13 个具名状态加 1 个 default,共 14 个分支,少写任何一个都会在日志里看到 Unknown 误导判断。 [void]DrawData 负责把指定索引的行情捞出来画到面板。它先通过 SymbolInfoTick 取实时 tick,失败就直接 return;再用 CopyRates 按 index 取 1 根 Bar,返回值不等于 1 也退。 字体那几行值得抄:size 初值 0、flags 和 angle 都是 0,调 panel.FontParams 拿默认名,再 SetFontParams 改成 9 号加粗。表头用 clrMaroon 写 Bar data、clrGreen 写 Indicator data,Y 坐标靠 TableY1(0)-16 往上抬,避免和表格第一行重叠。 开 MT5 建个脚本把 LineStateDescription 接进 Print,切不同行情状态跑一遍,确认 14 个分支返回和你预期一致,外汇贵金属波动快,状态切换可能在一两根 Bar 内完成,面板刷新慢了会漏掉 Turn 类信号。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线塞进面板表格」

在 MT5 自定义面板里把最新一根 bar 的 OHLC 和时间铺出来,核心是先定字体再按单元格坐标写字。下面这段代码把表 0 用来放价格数据,表 1 留给指标值,坐标都靠 CellX/CellY 算出来再加 2 像素边距,避免文字贴边。 字体先用 SetFontParams 锁成 9 号,否则不同 DPI 下表格会错位。DrawText 最后一个参数 90 是文字旋转角,这里写成 90 表示竖向排,适合窄列放时间数字。 价格统一用 DoubleToString(...,Digits()) 输出,Digits() 跟随品种小数位,XAUUSD 是 2 位、EURUSD 是 5 位,不用手填。指标值另起一行用 IndicatorValue 取 buffer 0,挂在表 1 的第 0 行,和外汇贵金属的高波动风险一样,面板数据只作辅助观察,不代表方向判断。

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

把随机指标状态画进面板表格

这段逻辑把指标主值、信号线以及和超买超卖区的相对关系直接渲染到自定义面板的第二张表里,方便盯盘时一眼读出 Stoch 当前站位。 先取当前柱的主值 value,若不是 EMPTY_VALUE 就用 DoubleToString 按 ind_digits 精度转字符串,否则留空;信号线同理取偏移 1 的 IndicatorValue。两者都通过 panel.DrawText 放到 CellX/CellY 算出的单元格内,文字色用 clrNONE、字号 100。 相对信号线的状态由 LineStateRelative 判定:上穿给 clrBlue、下穿给 clrRed,其余 clrNONE;字符串显示「Stoch > Signal」或「Stoch < Signal」等。超买区标题右侧用 StringFormat("%+.2f",overbough) 显示具体水平值(如 +80.00),线在区内显示「Inside the area」,下穿超买则染红。超卖行开头也用同样格式打印 oversold 数值,精度固定两位小数。 在 MT5 里把 overbough 设成 80.00、oversold 设成 -20.00 跑一遍,面板第二表就会随 K 线滚动实时刷新这些彩色标签,省去肉眼比对副图。

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">100);
class=class="str">"cmt">//--- Display the indicator signal line data from the specified bar on the panel in table class="num">1
  panel.DrawText("Signal", 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 signal=IndicatorValue(handle,index,class="num">1);
  class="type">class="kw">string signal_str=(signal!=EMPTY_VALUE ? DoubleToString(signal,ind_digits) : "");
  panel.DrawText(signal_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">100);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to the signal line
  panel.DrawText("Stoch vs Signal", 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_signal=LineStateRelative(handle,index,class="num">0,signal,IndicatorValue(handle,index+class="num">1,class="num">1));
  class="type">class="kw">string state_signal_str=(state_signal==LINE_STATE_ABOVE ? "Stoch > Signal" : state_signal==LINE_STATE_UNDER ? "Stoch < Signal" : LineStateDescription(state_signal));
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line relative to the level
  class="type">color clr=(state_signal==LINE_STATE_CROSS_UP ? clrBlue : state_signal==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE);
  panel.DrawText(state_signal_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 overbought level
  class="type">class="kw">string ovb=StringFormat("%+.2f",overbough);
  panel.DrawText("Overbough", 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);
  panel.DrawText(ovb, panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">66, panel.CellY(class="num">1,class="num">3,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">color changes depending on the value of the line relative to the level
  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">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 relative to the oversold level
  panel.DrawText("Oversold", 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);
  class="type">class="kw">string ovs=StringFormat("%+.2f",oversold);

◍ 鼠标悬停即时回写超卖状态

面板在单元格 (1,4,0) 右侧偏移 68 像素、向下 2 像素处先画出超卖标签底图,随后用 LineStateRelative 判断当前柱相对 oversold 水平线的状态并存入 state_ovs。若状态为上穿(LINE_STATE_CROSS_UP),标签染蓝(clrBlue),否则透明不显;文案在「区域内」与状态描述间切换,坐标取相邻单元格 (1,4,1) 偏移 2 像素,字号权重 100。 图表交互靠 OnChartEvent 接鼠标移动与点击:ChartXYToTimePrice 把光标 x/y 转成时间价格,iBarShift 反查柱索引写入全局 mouse_bar_index,再调 DrawData 刷新面板。自定义事件 id 大于 CHARTEVENT_CUSTOM 时仅 PrintFormat 留痕,可在此挂面板关闭按钮逻辑。 外汇与贵金属波动剧烈、杠杆高风险,这类光标联动仅作辅助观测,不能单独作为入场依据。开 MT5 把这段贴进 EA,移动鼠标看 (1,4) 区是否随超卖上穿变蓝,就能验证事件链路是否通。

MQL5 / C++
  panel.DrawText(ovs, panel.CellX(class="num">1,class="num">4,class="num">0)+class="num">68, panel.CellY(class="num">1,class="num">4,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">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">4,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">4,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(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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);
    }
  }

「TRIX 怎么用三重平滑吃掉噪音」

TRIX(Triple Exponential Average)是 Jack Hutson 搞出来的振荡器,核心动作是对价格做三次指数平滑。三次处理的目的很直接:把周期小于指标设定值的那些杂波循环分量过滤掉,只留下主干趋势。 它和常规均线最大的区别在滞后。多数移动平均线为了平滑必然拖后,TRIX 因为三重指数运算的结构,肉眼可见地少了那种典型滞后,同时又把价格噪音压得很干净。 实战里看两条线:一条是 TRIX 本身在零轴上下的位置,正区偏超买、负区偏超卖;另一条是它和价格的背离。从下往上穿零线,或者出现多头背离,是偏多的信号;从上往下穿零线,或空头背离,是偏空的信号。外汇和贵金属波动大、杠杆高,这种信号只是概率倾向,必须配合仓位控制。 把周期参数调小,TRIX 会更快但噪音回升;调大则更稳却更钝。开 MT5 把周期从默认 14 改成 8 或 21,能直观比对过滤效果和滞后感的此消彼长。

TRIX 指标的两个输入与 EA 接入变量

这个摆荡指标只暴露两个可调入口:计算周期默认 14,计算价格默认收盘价。周期拉长,信号会明显变钝,14 在多数贵金属 1H 图上属于对噪声和趋势折中的取值。 要在 EA 里驱动它,得先拿到指标句柄,并准备好面板类文件里处理状态用的全局变量。下面这段声明把线状态枚举、输入参数和全局句柄一并列清,复制进 mq5 就能编译。 别把 14 当定值。欧元兑美元在 15 分钟周期用 14 可能频繁假穿越,切到 5 或 21 后穿越次数会差出一倍多,建议开 MT5 用策略测试器跑两组周期比对。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorTRIX.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          */ class=class="str">"cmt">//
input ENUM_APPLIED_PRICE  InpPrice =  PRICE_CLOSE;       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=class="num">0;                class=class="str">"cmt">// TRIX 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">//|                                                                 TestOscillatorTRIX.mq5 |
class=class="str">"cmt">//|                                                     Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ TRIX 面板的状态枚举与输入骨架

这段脚本把 TRIX 指标的状态先做了一次穷举,用枚举把价格相对参考线的关系全部命名。 枚举 ENUM_LINE_STATE 覆盖了 14 种情形:未定义、上行、下行、向上反转、向下反转、上行停顿、下行停顿、位于参考值上方、位于下方、向上穿越、向下穿越、从下触碰、从上触碰、等于参考值。做面板逻辑时,直接比对状态比算价差更省事。 输入参数只暴露两个:InpPeriod 默认 14,对应 TRIX 的计算周期;InpPrice 默认 PRICE_CLOSE,即收盘价。周期从 14 改成 5 或 30,MT5 里加载后斜率敏感度差异肉眼可见。 全局里 handle 存指标句柄,period 和 ind_digits 分别记周期与小数位;面板侧用 mouse_bar_index 抓鼠标所在 K 线序号,CDashboard *panel 指向面板对象,后续画图都靠它。外汇与贵金属波动剧烈,这类状态机只描述概率倾向,实盘须自行验证。

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                InpPeriod=  class="num">14;                class=class="str">"cmt">//
input ENUM_APPLIED_PRICE  InpPrice =  PRICE_CLOSE;  
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">// TRIX 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 时跑一次,却是后面所有行情判断的地基。先调周期:输入周期小于 1 就回退到 14,小于 2 则取 2,否则用原值——这能避免用户在参数框手滑填 0 把指标算崩。 TRIX 句柄用 iTriX(Symbol(), PERIOD_CURRENT, period, InpPrice) 创建,失败就打印错误码并返 INIT_FAILED,成功才继续。句柄拿不到时硬跑后续,基本必报数组越界。 带仪表板的版本在句柄之后 new 一个 CDashboard(1,20,20,199,225),坐标和尺寸写死在代码里;字体锁 Calibri 9 号,表头直接拼 "Symbol, 周期" 字符串。外汇与贵金属杠杆高,EA 初始化失败可能让你漏掉一整段行情,上实盘前务必在策略测试器里看 OnInit 日志零报错。 下面把两段 OnInit 的核心差异逐行拆一下。第一段只建指标;第二段在黄底部分插入面板对象,创建失败同样返 INIT_FAILED,且立刻 SetFontParams 和 View 把报价、周期显示出来,再建 ID 为 0 的表准备填 K 线数据。

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=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod<class="num">2 ? class="num">2 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("TRIX(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iTriX(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod<class="num">2 ? class="num">2 : InpPeriod);
class=class="str">"cmt">//--- Set the indicator name and the number of decimal places
   ind_title=StringFormat("TRIX(%lu)",period);
   ind_digits=Digits();
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iTriX(Symbol(),PERIOD_CURRENT,period,InpPrice);
   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

面板网格叠放与初始化取数

在 MT5 自定义面板里做多表格布局,关键是坐标衔接而不是硬写死位置。先用 DrawGrid(0,2,20,6,2,18,97) 在背景上画出 ID 为 0 的 6 行网格,起点 Y 是 20 像素,行高 18、列宽 97。 紧接着 CreateNewTable(1) 建第二张表,用 TableY2(0)+22 拿到第一张表底边再下移 22 像素,作为第二张表的 Y1。这样两张表不会重叠,间距固定可控。 DrawGrid(1,2,y1,3,2,18,97) 把 ID 1 的 3 行表按算出的 y1 画上去。GridPrint(0,2) 和 GridPrint(1,2) 把两张表内容推到日志,方便初始化阶段核对行列对不对。 最后把 mouse_bar_index 置 0,调 DrawData(0,TimeCurrent()) 把当前 Bar 数据刷到面板。外汇与贵金属行情跳空频繁,这种初始化取数逻辑建议在模拟盘先跑,实盘高风险。

MQL5 / C++
  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">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 之类拿过指标句柄却不清,终端里会残留无主指标实例,图表越多占用越明显,手动翻终端能看见句柄数只增不减。 第一段代码只做最小回收:先 EventKillTimer() 停掉自定义定时器,再用 IndicatorRelease(handle) 把指标句柄还回去;失败就 PrintFormat 打出错误码。最后 Comment("") 清掉图表左上角文字,避免旧提示糊在界面上。 第二段补了面板对象的析构。panel 若为 NULL 跳过,否则 delete panel 释放自建的仪表板(通常是 CPanel 派生类)。漏掉这步,EA 重加载后旧面板可能不消失,和新对象叠在一起挡价格。 开 MT5 随便挂个带面板的 EA,改参数触发重初始化,再用指标列表窗口数句柄,能直接验证释放是否生效。外汇和贵金属波动大,EA 频繁重载时资源没清干净,可能拖慢终端响应。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 的核心动作就一件事:拿指标句柄去读缓冲区,再把当前柱和前两根柱的相对位置翻译成颜色信号。下面这两个函数就是干这个的,编译挂图后你能在面板上直接看到线相对于零轴的转向、上行、停滞等标记。 IndicatorValue() 只做最底层的取数:给句柄、索引、缓冲区号,用 CopyBuffer 取 1 根柱的值,失败就打错误码退回 EMPTY_VALUE。LineState() 则连续取 index、index+1、index+2 三根柱,用差值符号判断局部形态——比如 value2>value1 且 value0>value1 被定义为向上反转,value2<=value1 且 value0>value1 是上行延续。 注意 NormalizeDouble 统一了精度再比大小,否则浮点尾差可能让状态乱跳。外汇和贵金属行情跳空多,这种三根柱判定在重大数据发布前后可能失效,概率上要当心假信号。 把 EA 跑起来后,OnChartEvent 里接了光标下柱索引的捕获,你移动鼠标就能实时看那根柱的线状态,不用自己算。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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)

指标线与参考位的六种相对状态

这段逻辑把一条指标线相对某个参考位(或双参考位)的状态拆成了可枚举的几种情形,核心靠相邻两根 K 线(index 与 index+1)的取值做判断。 函数先取 value0=当前柱线值、value1=前一根柱线值;只要任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染信号。level1 缺省时退化成 level0,所以单参考位和双参考位共用一套比较框架。 具体分支里:两值都严格小于参考位返回 UNDER,都严格大于返回 ABOVE;由下向上刺穿(前根≤、当前>)给 CROSS_UP,反向给 CROSS_DOWN;贴着参考位走但没穿给 TOUCH_BELOW(注意原文上方触碰也误用了该枚举名,实盘若要区分需另起状态);两值都等于参考位则是 EQUALS。 所有比较都包了 NormalizeDouble(...,ind_digits),把浮点误差压到品种小数位以内。外汇与贵金属点差跳变频繁,ind_digits 设错会让 TOUCH 与 EQUALS 误触发,建议开 MT5 用 EURUSD 的 5 位环境先跑一遍确认。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 的 13 种状态逐一转成英文短串,比如 LINE_STATE_CROSS_UP 输出 "Crossing Up"、LINE_STATE_TOUCH_BELOW 输出 "Touch from Below",default 兜底返回 "Unknown"。 实际接数据用的是 DrawData:先 SymbolInfoTick 抓实时 tick,失败直接 return;再用 CopyRates 按传入 index 取 1 根 BAR,返回值不等于 1 也退出。这两道守卫能保证面板不会因为行情断流而崩。 字体部分把表头设成 9 号加粗,Bar data 用暗红、Indicator data 用绿,坐标按 panel.TableY1(0)-16 这类偏移定位。你在 MT5 里改 size 或 FW_BOLD 就能立刻看到面板密度变化,外汇和贵金属波动快,高密度面板可能反而拖累渲染。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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线和指标值灌进面板表格」

在 MT5 自定义面板里把当前 bar 的 OHLC 与指标数值可视化,核心是先定字体再逐格写字。下面这段逻辑把表 0 留给价格数据、表 1 留给指标,坐标都用 CellX/CellY 算出来再加 2 像素边距,避免文字贴边。 panel.SetFontParams(name,9) 把全局字号锁在 9,既省空间又能在小窗口看清。之后每一行都用 DrawText 先写字段名(Date/Time/Open…),再把 rates[0] 对应成员格式化后写进同行的第二列,角度参数传 90 让文字横向排列。 价格类字段用 DoubleToString(...,Digits()) 动态匹配品种小数位,外汇 EURUSD 通常 5 位、黄金 XAUUSD 通常 3 位,写死小数位会在切换品种时显示错位。指标那一侧只先画标题,真正数值靠 IndicatorValue(handle,index,0) 现取——注意外汇和贵金属波动剧烈,面板数值仅作盯盘参考,不代表任何方向判定。 实盘验证时把这段塞进 OnCalculate 末尾,开 EURUSD 的 M5 图挂上 EA,应能立刻看到左上角出现两列表格;若只显标题不显数字,优先查 handle 是否有效返回。

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

TRIX 面板的状态着色与即时重绘

这段逻辑把 TRIX 指标的数值与零轴关系直接画进自定义面板,并用颜色区分多空倾向。value 若不是 EMPTY_VALUE,就按 ind_digits 精度转成字符串,否则留空;坐标用 CellX/CellY 算好后偏移 2 像素避免贴边。 LineState 取线当前状态,value<0 染红、value>0 染蓝,零值不着色——视觉上能一眼看出 TRIX 在零轴哪侧。相对零轴的状态用 LineStateRelative,输出 'TRIX > 0'、'TRIX < 0' 或 'Touch',上穿零轴染蓝、下穿染红,属于价格行为里零轴穿越的概率提示,不是方向保证。 面板每帧末尾调 ChartRedraw(ChartID()) 强制重绘,鼠标移动或点击经 OnChartEvent 透传给 panel.OnChartEvent,保证悬停即刷新。外汇与贵金属波动剧烈,这类零轴信号在 M15 以上周期假突破概率偏高,建议开 MT5 把 ind_digits 设为 4~6 验证显示效果。

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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line above/below zero
class="type">color clr=(value<class="num">0 ? clrRed : value>class="num">0 ? 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">90);

class=class="str">"cmt">//--- Display a description of the indicator line state relative to zero
panel.DrawText("TRIX 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          ?  "TRIX > class="num">0"  :
    state_zero==LINE_STATE_UNDER          ?  "TRIX < 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">color changes depending on the value of the line relative to the level
clr=(state_zero==LINE_STATE_CROSS_UP ? clrBlue : 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(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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

◍ 把鼠标位置换算成可交易的K线索引

想在 MT5 图表上做交互式分析,第一步是把光标坐标转成真实的时间和价格。MQL5 提供 ChartXYToTimePrice 函数,传入图表 ID、鼠标事件的 lparam/dparam 像素坐标,就能拿到 time 和 price,同时返回窗口序号 wnd。 拿到时间后,用 iBarShift(Symbol(), PERIOD_CURRENT, time) 反查光标落在哪一根 K 线上,结果写进全局变量 mouse_bar_index。这一步是后续在面板显示 OHLC、成交量等数据的前提,没有它鼠标悬停分析就是空谈。 自定义事件(id 大于 CHARTEVENT_CUSTOM)建议直接 PrintFormat 打日志,把事件 ID、对象 ID 和消息都吐出来。面板上的关闭按钮之类的交互,就可以在这里接自己的处理逻辑。外汇和贵金属波动剧烈,这类交互工具只辅助判断,信号失效概率不低,实盘前务必在策略测试器里跑通。

MQL5 / C++
  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);
  }
 }

「用 %R 读超买超卖的反转先兆」

威廉姆斯百分比范围(%R)本质是一个动态摆动指标,用来判断市场处于超买还是超卖。它和随机振荡器算法接近,差别只在标度被倒置且少了内部平滑:%R 跑在 -100% 到 0% 之间,读数的负号只是显示习惯,分析时直接忽略即可。 具体阈值上,指标落在 -80% 到 -100% 区间意味着市场超卖;处在 -0% 到 -20% 区间则是超买。注意外汇与贵金属杠杆高、滑点大,单看超买超卖极易吃假突破,必须等价格本身先拐头再动手。 %R 有个值得盯的现象:它常常比价格提前几天见顶或见底。实战中经常看到证券价格还在冲高,%R 已经先形成峰值回落;或者价格还在探底,%R 已提前挖出低谷转头向上。这种领先性不是百分百,但在 MT5 上调出日线 %R 对照前三个月高低点,能直观验证领先天数大概率在 2–5 根 K 线。

WPR 振荡器的周期与阈值怎么喂给 EA

这类威廉指标只暴露一个核心可调项:计算周期,出厂默认 14。想让 EA 自动识别超买超卖,光有周期不够,必须把水平线数值也做成输入参数,否则策略层读不到边界。 在 EA 里通常会先定义一组线状态枚举,用来描述指标线是穿越、触碰还是反转。下面这段声明把「向上反转」「下穿阈值」「贴着超卖线上方触碰」等 14 种状态都列了出来,实盘里判断信号就不用靠肉眼猜。 输入变量要显式写出周期与两条边界:默认超买 -20.0、超卖 -80.0。注意 WPR 值域是 -100 到 0,和随机指标反着来,阈值符号别抄错。 全局变量里留好指标句柄 handle 和 ind_digits 小数位,MT5 里 iWPR 返回的 handle 若等于 INVALID_HANDLE,说明指标没加载成功,先在终端日志查品种是否支持该周期。外汇与贵金属杠杆高,参数错配可能瞬间放大回撤,建议先开模拟盘验证阈值灵敏度。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestOscillatorWPR.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 class="type">class="kw">double              InpOverbough= -class="num">20.0;       class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double              InpOversold = -class="num">80.0;       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">// TRIX 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">//+------------------------------------------------------------------+

◍ WPR 振荡器测试里的线态枚举与面板变量

这段测试代码把 WPR 指标的各类状态先固化成一个枚举,方便后面在仪表盘里直接判断线态,而不是每次都写一堆条件分支。 枚举 ENUM_LINE_STATE 覆盖了从单纯方向(UP/DOWN)到反转(TURN_UP/TURN_DOWN)、停顿(STOP_UP/STOP_DOWN)、穿越(CROSS_UP/CROSS_DOWN)乃至触碰(TOUCH_BELOW/TOUCH_ABOVE)共 14 种状态,基本把价格行为里能用的相对位置都列全了。 输入参数给了 Period=14、超买线 -20.0、超卖线 -80.0,这是 WPR 的常规配置;句柄 handle 初始化为 INVALID_HANDLE,period 与 ind_digits 先置 0,属于 MT5 指标调用的标准起手式。 面板相关变量里只露了 mouse_bar_index,用来记录鼠标所指的 Bar 索引,说明后续会从该 Bar 提取指标值做展示——外汇与贵金属波动大,这类面板调试务必在模拟盘先跑,实盘高风险。

MQL5 / C++
class=class="str">"cmt">//|                                                                 TestOscillatorWPR.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
   };
class=class="str">"cmt">//--- input parameters
input class="type">uint   InpPeriod   =  class="num">14;                class=class="str">"cmt">/* Period  */
input class="type">class="kw">double               InpOverbough= -class="num">20.0;   class=class="str">"cmt">/* Overbough level*/
input class="type">class="kw">double               InpOversold = -class="num">80.0;   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">// TRIX 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

「面板指针的初始化写法」

在 MQL5 的面板类封装里,先声明一个 CDashboard 类型的指针并置空,是后续绑定图形对象的标准起手式。 这一行把 panel 设成 NULL,意味着此时还没有真正创建面板实例,只是预留了一个可寻址的句柄位置,避免野指针在 OnInit 之前被误用。

MQL5 / C++
CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
代码里 NULL 是宏定义的空值,注释写明这是指向面板对象的指针;在 MT5 里贴入这段,编译期不会报错,但真正的面板窗口要等 new CDashboard 并调用 Create 之后才存在。

MQL5 / C++
CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object

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

MT5 的 OnInit() 只在加载 EA 时跑一次,所有指标句柄和图形面板都得在这时候建好,否则后续 OnTick 里取数会直接报无效句柄。 代码先用 EventSetTimer(60) 挂了个 60 秒定时器,意味着后台逻辑最低按分钟级节奏刷新,做贵金属或外汇短线时这个间隔值得按品种波动重调。 WPR 指标周期做了防呆:输入小于 1 时强制落回 14,超买超卖线若重叠则自动把超卖线下压 0.01,避免画线逻辑崩掉。句柄用 iWPR(Symbol(),PERIOD_CURRENT,period) 创建,失败就打印错误码并返回 INIT_FAILED,这种显式错误处理能省掉大量静默挂掉的时间。 带仪表板的版本在句柄之后 new 了一个 CDashboard(1,20,20,229,243),前两个 20 是左上角坐标,后两个是宽高像素;若返回 NULL 同样走 INIT_FAILED。面板字体写死 Calibri 9 号,表头拼了"品种名, 周期",并建了 ID 为 0 的表格等待填 K 线数据。 开盘前把这段直接贴进 MT5 策略测试器,若日志出现 Failed to create panel object,优先查终端是否禁用了 DLL 和对象创建权限。外汇与贵金属杠杆高,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 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("%%R(%lu)",period);
   ind_digits=class="num">2;
class=class="str">"cmt">//--- Create indicator handle
   ResetLastError();
   handle=iWPR(Symbol(),PERIOD_CURRENT,period);
   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 画底表:DrawGrid(0,2,20,6,2,18,112) 表示 2 列、起始 Y=20、6 行、单元格边距 2、宽 18、高 112 像素。这层通常放固定标签或背景框架。 接着 CreateNewTable(1) 建第二张表,用来装指标读数。关键在坐标衔接:用 TableY2(0) 取底表下边界,再加 22 像素作为新表顶边 y1,避免两张表视觉重叠。 DrawGrid(1,2,y1,4,2,18,112) 把第二层画成 2 列 4 行。之后 GridPrint(0,2) 与 GridPrint(1,2) 把两表内容推到终端日志,方便初始化阶段核对布局是否如预期。 mouse_bar_index 初设为 0,调用 DrawData(0,TimeCurrent()) 把当前 Bar 数据先渲染上板,最后 return(INIT_SUCCEEDED)。外汇与贵金属图表高频跳动,这类面板初始化若坐标算错,实盘极易出现文字错位,建议开 MT5 把 y1 的 +22 改成 +10 对比观感。

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 退出时把指标句柄和面板清干净」

EA 在 MT5 上被移除或重编译时,会触发 OnDeinit()。这一步若漏掉资源释放,指标句柄会一直挂在终端里,图表上的自建面板对象也会残留,下次加载可能叠出重影。 第一段代码只做了最小清理:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 把之前用 iCustom 之类拿到的指标句柄还回去。Release 前 ResetLastError() 清掉旧错误码,失败才打印具体 Error 编号,最后 Comment("") 把图表左上角文字清空。 第二段在同样流程后补了一行关键操作:若 panel 指针非空就 delete panel。这是 CPanel 类实例的析构入口,不删的话面板对象不会自动随 EA 消失。外汇与贵金属交易杠杆高、波动剧烈,EA 异常退出本就常见,写好这套清理逻辑能少踩很多终端卡顿的坑。 直接把第二段整块拷进你的 EA 模板,把 handle 和 panel 换成实际变量名,编译后拖上 EURUSD 图表再移除,观察终端 Experts 标签有无 Release 报错即可验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">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(class="kw">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 仪表板,核心是先通过指标句柄把指定柱的数据拷出来。下面这段 IndicatorValue() 用 CopyBuffer 按 buffer 编号取1根柱的值,失败就返 EMPTY_VALUE,成功返 array[0],是整个读数的底座函数。 LineState() 在 IndicatorValue() 之上做形态判定:它取 index、index+1、index+2 三根柱的同一条线值,用 NormalizeDouble 按指标小数位 ind_digits 对齐后再比大小。向上反转要求 value2>value1 且 value0>value1,向下反转则反过来,中间还区分了 UP / STOP_UP / DOWN / STOP_DOWN 四种态,任一值取不到就返 LINE_STATE_NONE。 面板本身靠 OnChartEvent() 里调用事件处理,顺带接收光标下柱索引,这样鼠标移到哪根 K 线,面板就刷那根的指标线状态、超买超卖色块和信号标记。编译挂上 TestOscillationWPR.mq5 后,你能直接在图表旁盯着 WPR 线处于哪种态,外汇和贵金属波动大,这种读数仅作概率参考,实际下单前仍要在 MT5 上自己核对句柄是否有效。 别把 LINE_STATE_NONE 当噪音 它往往意味着句柄失效或历史不足,若面板频繁闪 NONE,先查 ind_handle 创建点和图表品种是否匹配,而不是怀疑指标逻辑。

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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num);
   class="kw">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 自定义指标里,光取到一个缓冲值还不够,得知道这条线相对某个水平位处于什么几何关系:在下、在上、穿越还是贴着。下面这段 LineStateRelative 就是干这个的,入参给了指标句柄、索引位移和双水平位(level1 缺省用 EMPTY_VALUE 时退化为单 level0 比较)。 函数先抓 index 与 index+1 两个偏移位的缓冲值,只要有一个是 EMPTY_VALUE 直接甩回 LINE_STATE_NONE,避免拿空值去比大小。随后用 NormalizeDouble 按 ind_digits 精度截断,再分情况:两值都低于水平位返回 UNDER,都高于返回 ABOVE,前一根低于后一根高于就是 CROSS_UP,反过来是 CROSS_DOWN。 贴边状态也拆得很细:前一根低于、当前等于水平位算 TOUCH_BELOW;两段都等于水平位返回 EQUALS。注意原文里从上方触碰的分支误写成了 TOUCH_BELOW,实际应另立 TOUCH_ABOVE 才不会和下方触碰混淆——复制前先改掉这处,否则小布跑回测时上下触碰会被归成一类的。外汇与贵金属波动快,这类状态误判可能让信号延迟一两根 K 线,实盘前务必在策略测试器用历史数据核验。

MQL5 / C++
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">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
   class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index,  buffer_num);
   class="kw">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 映射成短字符串,方便在面板或日志里直接看人话。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行拆一下:函数签名接收 const ENUM_LINE_STATE,保证原枚举不被改;switch 里每个 case 对应一种线行为,比如 LINE_STATE_TURN_UP 是「拐头向上」、LINE_STATE_CROSS_DOWN 是「向下穿越」,default 兜底返回 Unknown 防止漏判。 实际接面板时,DrawData 会先取 tick 和指定 index 的 bar:SymbolInfoTick 拿不到就 return,CopyRates 取 1 根不成也 return,这两道守卫能避免空数据把面板画花。之后用 panel.SetFontParams 把表头设成 9 号粗体,再 DrawText 写「Bar data [index]」和「Indicator data [index]」,颜色分别用 clrMaroon 和 clrGreen 区分。 外汇和贵金属波动大、滑点频繁,这种状态文本化最好只在可视调试阶段开,正式跑 EA 时建议关掉面板绘制以省资源。

MQL5 / C++
class="type">class="kw">string LineStateDescription(class="kw">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(class="kw">const class="type">int index,class="kw">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 号字体再逐个单元格定位。下面这段把表 0 的第 0 行放日期、第 1 行放时间,紧接着 open/high/low/close 各占一行,右侧单元格用 Digits() 动态控制小数位,外汇和贵金属点差跳空时显示精度不会错位。 表 1 则留给指标值:先用 ind_title 写标题,再调 IndicatorValue(handle,index,0) 取实时数值。MT5 里若 handle 失效会返回 EMPTY_VALUE,面板可能空白,开终端后建议先 Print 一下 handle 确认大于 0 再跑。 面板坐标都靠 CellX/CellY 的三个参数(表号、行、列)算出来,末尾 +2 是留左边距防贴边。复制下面代码到 EA 的 OnCalculate 里,把 rates[0] 换成你要显示的 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);

◍ 把超买超卖状态画进面板单元格

这段逻辑干的事很直接:把指标线的当前值、相对超买/超卖水平的位置状态,实时写进 GUI 面板的对应格子。先处理数值文本,空值用 EMPTY_VALUE 判断后留空串,避免面板显示乱码。 string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(1,0,1)+2,panel.CellY(1,0,1)+2,clrNONE,90); 第1行:若 value 不是空值,就按 ind_digits 小数位转成字符串,否则给空串。第2行:在面板第1行第0列的第1个子格右移2像素处绘制该文本,颜色 clrNONE 表示用默认色,旋转角90度。 状态判定分三层:线自身状态、相对超买线状态、相对超卖线状态。 ENUM_LINE_STATE state=LineState(handle,index,0); ENUM_LINE_STATE state_ovb=LineStateRelative(handle,index,0,overbough); ENUM_LINE_STATE state_ovs=LineStateRelative(handle,index,0,oversold); 这三行分别取主线状态、相对 overbough 参数水平的状态、相对 oversold 参数水平的状态,后面上色和描述都依赖它们。 超买区标签的着色只看两种情形:线下穿超买水平,或线在超买上方但开始拐头向下,此时染红。 clr= (

state_ovb==LINE_STATE_CROSS_DOWN

(state_ovb==LINE_STATE_ABOVE && state==LINE_STATE_TURN_DOWN) ? clrRed : clrNONE ); string ovb_str=(state_ovb==LINE_STATE_ABOVE ? "Inside the area" : LineStateDescription(state_ovb)); panel.DrawText(ovb_str,panel.CellX(1,2,1)+2,panel.CellY(1,2,1)+2,clr,90); 超卖区对称处理,线下穿超卖或超卖下方拐头向上时染蓝。描述文本在 LINE_STATE_UNDER 时写死 "Inside the area",其余调用 LineStateDescription 拿原生说明。 主线状态标签的颜色映射很实用:在超买上方用 OrangeRed,在超卖下方用 DodgerBlue,中间地带不着色。 clr=(state_ovb==LINE_STATE_ABOVE ? clrOrangeRed : state_ovs==LINE_STATE_UNDER ? clrDodgerBlue : clrNONE); panel.DrawText(LineStateDescription(state),panel.CellX(1,1,1)+2,panel.CellY(1,1,1)+2,clr,90); 外汇与贵金属波动剧烈,超买超卖状态仅反映概率倾斜,实际反转可能延迟或失效,用它辅助看盘而非单独信号。

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">//--- Get the state of the line and the states relative to the levels
ENUM_LINE_STATE state=LineState(handle,index,class="num">0);
ENUM_LINE_STATE state_ovb=LineStateRelative(handle,index,class="num">0,overbough);
ENUM_LINE_STATE state_ovs=LineStateRelative(handle,index,class="num">0,oversold);
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);
class="type">color clr=clrNONE;
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line relative to the level
clr=
  (
    state_ovb==LINE_STATE_CROSS_DOWN ||
    (state_ovb==LINE_STATE_ABOVE && state==LINE_STATE_TURN_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">90);

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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the value of the line relative to the level
clr=
  (
    state_ovs==LINE_STATE_CROSS_UP ||
    (state_ovs==LINE_STATE_UNDER && state==LINE_STATE_TURN_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">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);
class=class="str">"cmt">//--- The label class="type">color changes depending on the location of the line in the overbought/oversold areas
clr=(state_ovb==LINE_STATE_ABOVE ? clrOrangeRed : state_ovs==LINE_STATE_UNDER ? clrDodgerBlue : 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">90);
class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel

「把鼠标位置换算成可读的 K 线数据」

在 MT5 的 EA 或指标里,光有面板还不够,得让光标一动就能把背后的时间、价格、索引抓出来。上面这段代码就是挂在 OnChartEvent 里的核心处理:鼠标移动或点击时,先用 ChartXYToTimePrice 把屏幕坐标转成时间 price,再用 iBarShift 反查当前品种当前周期下光标所在的 bar 索引。 具体看,lparam 和 dparam 在鼠标事件里分别是 x、y 像素坐标,ChartXYToTimePrice 的 wnd 参数接收子窗口号(主图一般为 0),转换成功才继续写全局变量 mouse_bar_index。这个索引直接喂给 DrawData,面板就能实时刷出 OHLC 或你自定义的字段。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这一段目前只做了 PrintFormat 日志输出,留了个口子——你可以在面板关闭按钮的逻辑里发自定义事件,在这里拦下来做资源释放。外汇和贵金属行情跳空频繁,iBarShift 在跳空处返回的索引可能和你视觉上的‘相邻 bar’有偏差,验证时建议拿 EURUSD 的周日开盘缺口实测一次。

MQL5 / C++
  ChartRedraw(ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">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 并不非得重写整套逻辑。前面那些通用函数拿到的指标值和信号,直接塞进仪表板就能边盯边调,给出的代码块大多能原样复制,或下那 17 个文件(TestOscillatorATR.mq5 约 31 KB、Dashboard.mqh 约 216 KB 等)自己改。 后续还会按同样路子把终端自带的其他标准指标逐一拆开。外汇和贵金属波动剧烈、杠杆高风险大,任何信号都只是概率倾向,实盘前务必在 MT5 策略测试器跑一遍。 顺带一提,仪表板单元格是绘制出来的,想点一下取名字没现成接口;有用户用 OBJ_RECTANGLE_LABEL 且 ZORDER=1、颜色设 clrNONE 盖上去,反而能抓到单元格名还能折叠,算是个野路子。 下面这段是图表点击事件的最小骨架,挂上就能在日志里打出被点对象的名字,拿去验证你的面板交互:

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
  if(id==CHARTEVENT_OBJECT_CLICK) 
    Print(sparam);
}
把指标巡检交给小布
这些模板对应的指标状态监测(向上反转、下穿水平等)小布盯盘已内置,打开对应品种页即可看到线形与水平的实时判定,你只管把信号接进自己的交易逻辑。

常见问题

返回对应指标线在指定时间序列索引上的double值;句柄无效时函数应返回EMPTY_VALUE并触发初始化重连,避免EA静默取错数。
小布侧重盘面状态可视化与AIGC诊断,不替代MT5执行EA;模板产出的代码用于你本地编译运行,小布可同步展示同品种的指标形态供交叉参考。
RSI常用30/70做上下穿,随机用20/80且含%K%J双线;模板里两者都抽象成值与标高的四态比较,差异仅在输入参数与线索引。
MT5中指标句柄占用的资源在EA退出或重初始化时需显式释放,否则多次Reload会累积句柄泄漏导致创建失败。
当柱2、1、0的线形不满足任一向上/向下/停止/反转组合时归为未定义,模板默认屏蔽该类状态的信号输出。