为EA交易提供指标的现成模板(第2部分):交易量和比尔威廉姆斯指标·综合运用
- 把线段状态映射成可读字符串
- 把K线字段塞进面板表格
- 把光标位置喂给面板做实时读数
- 用 MFI 抓资金背离与极端区
- iMFI 句柄与 EA 输入参数的落地写法
- RSI 超卖阈值与线态枚举的落地写法
- RSI面板的全局变量与输入口
- EA 启动时的句柄与面板挂载
- 在面板上叠放两张数据网格
- EA卸载时句柄与面板对象的清理
- 从指标句柄抓数并判线态
- 指标线与阈值级别的六种相对状态判定
- 把指标线状态映射成可读文本
- 把当前K线塞进自定义面板表格
- 面板里把超买超卖状态画出来
- 把鼠标坐标变成可读的K线数据
- 用 OBV 读聪明钱的进出节奏
- OBV 句柄的参数与声明方式
- 给指标状态机先铺好枚举与面板骨架
- EA 启动时要先绑好指标句柄和面板
- 面板网格与光标柱数据的初始化衔接
- EA退出时把指标句柄和面板清干净
- 用句柄把指标值搬进面板
- 指标线与关卡位置的六种状态判定
- 把线态枚举翻译成可读标签
- 把K线字段塞进面板表格的画法
- 把光标位置喂给面板做实时回显
- iVolumes 句柄的参数与 EA 声明方式
- 正文
- EA 启动时的句柄与面板挂载
- 网格绘制与光标索引的初始接线
- EA卸载时别忘了释放指标句柄
- 用句柄把指标值搬进面板
- 指标线与参考位的相对状态判定
- 把指标线状态映射成可读文本
- 把 K 线字段铺进面板表格
- 把光标下的指标状态画进面板
- 面板关闭事件与比尔威廉姆斯指标分组
- AC 指标怎么提前半步抓转向
- 加速振荡指标的句柄与参数声明
- 给指标状态机先铺好枚举与句柄
- EA 启动时的指标句柄与面板挂载
- 面板网格与光标柱初始化衔接
- EA退出时如何干净释放指标与面板
- 指标句柄取数与面板着色逻辑
- 指标线与参考位的相对状态判定
- 把线状态枚举翻成可读文本
- 把当前K线字段铺到面板表格里
- 鼠标悬停读出光标处指标状态
- 面板关闭按钮的事件回调怎么接
- 用鳄鱼线分辨盲鸡行情
- 参数
- 鳄鱼指标的参数与句柄初始化
- 鳄鱼三线的状态枚举与输入参数拆解
- 面板与唇线变量的初始化清单
- EA 启动时的句柄与面板挂载
- 面板表格的布局与初始化绘制
- EA退出时把指标句柄和面板清干净
- 从指标句柄抓数据的两套函数
- 指标线与参考位的相对状态判定
- 把指标线状态翻译成人话
- 在自定义面板上铺开 K 线明细
- 把鳄鱼三线状态画进自定义面板
- 把鳄鱼唇线与齿线的相对状态画到面板上
- 面板关闭按钮的事件落点
- AO 碟形与零轴交叉的触发条件
- AO指标句柄的声明与参数接口
- 正文
- EA 启动时的指标句柄与面板挂载
- 把网格数据刷进日志并锁定光标柱
- EA退出时把指标句柄和面板清干净
- 从句柄抓数与零线状态判定
- 指标线与指定水平的相对状态判定
- 把指标线状态映射成可读字符串
- 把 K 线字段塞进面板表格的写法
- 在面板里把 AO 状态画出来
- 把鼠标位置翻译成K线索引
- 用分形抓顶底,但先过鳄鱼那关
- 分形指标句柄的入参与内存回收
- EA 启动时的句柄与面板挂载
- 面板网格与光标柱的初始化接驳
- EA退出时别忘了释放指标句柄
- 从指标句柄抓缓冲值的落地写法
- 用枚举把指标线相对价位的状态拆干净
- 把线态枚举翻成可读字符串
- 把当前K线塞进面板表格
- 鼠标悬停实时回写分形数值
- 用双直方图读鳄鱼线的咬合与张口
- 参数
- 鳄鱼线状态枚举与输入参数拆解
- 鳄鱼指标状态枚举与参数骨架
- 面板与均线周期的变量落地
- EA 启动时的句柄与面板落地
- 面板双表格的绘制与初始数据填充
- EA卸载时句柄与面板对象的清理
- 面板怎么把指标数值读出来
- 指标线与参考位的相对状态判定
- 把线态枚举翻成可读文字
- 在自定义面板上打印K线字段与指标值
- 把双直方图指标塞进自定义面板
- 把鼠标所在 K 线塞进全局变量
- BW MFI 与量能的四象限读法
- BWMFI 句柄与参数怎么挂进 EA
- 给指标状态机配上面板指针
- EA 启动时的句柄与面板装配
- 把网格数据刷进日志并锁定光标柱
- EA退出时别忘了释放指标与面板
- 从指标句柄抓数与线态判定
- 指标线与指定水平的相对状态判定
- 把指标状态翻译成人话的函数
- 把当前K线数据塞进面板表格
- 把 MFI 与成交量状态叠进面板单元格
- 把鼠标位置翻译成 K 线索引
- 面板表格坐标与折叠状态的持久化修复
- 给表格框定坐标与尺寸的取值器
- 表格行与单元格的计数逻辑
- 仪表盘类的私有成员与构造拆解
- 面板字体与表格绘制的接口拆解
- 取表格单元格坐标的封装写法
- 表格坐标与单元格的取值兜底
- 面板坐标与像素存档的变量命名
- 析构时如何保住面板状态
- 面板收起与文字绘制的内存账本
- 仪表盘里的文本擦写与表格登记逻辑
- 按名称取表对象与网格底图绘制
- 表格坐标与网格线的绘制逻辑
- 网格自动填充时的单元格坐标落点
- 画表格网格时顺手把行对象建好
- 把表格行渲染进画布并落盘像素
- 像素数组的落盘与回读容错
- 别急着下结论
把线段状态映射成可读字符串
做价格行为面板时,最怕状态枚举直接打印成数字,肉眼根本分不清当前是上破还是回踩。下面这个函数把 ENUM_LINE_STATE 的 13 种状态逐一转成英文短词,方便往 GUI 表格里写。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把K线字段塞进面板表格」
在自建面板对象里把当前 bar 的 OHLC 和时间打印出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格 0 的对应单元格。注意每个 DrawText 都给列 1 的格加了 +2 像素偏移,避免文字贴边被边框吃掉,这在 MT5 自定义面板里是高频踩坑点。 时间拆分用了 TIME_DATE 和 TIME_MINUTES 两次 TimeToString,分别落进第 0 行和第 1 行;价格类字段统一走 DoubleToString(..., Digits()),Digits() 动态跟随品种小数位,黄金(XAUUSD) 跑出来就是 2 位、部分外汇交叉盘可能是 3 或 5 位。 表格 1 用来放指标值,先写标题 ind_title,再调 IndicatorValue(handle, index, 0) 取 0 号 bar 的数值。这套写法如果直接挂 EURUSD 的 M1 上,开 MT5 按 F5 编译就能看到面板实时刷出最近一根 bar 的六个字段,外汇和贵金属波动剧烈,面板数值仅作参考、实盘高风险。
panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);
◍ 把光标位置喂给面板做实时读数
这段逻辑干的事很直接:鼠标在图上动一下,就把对应 K 线的时间和价格抓出来,写进全局变量再刷新面板。外汇和贵金属行情跳空频繁,这种随光标联动的读数能帮你快速核对某根 bar 的指标值,但高频重绘本身有轻微性能开销,实盘前建议在 MT5 策略测试器里跑一遍看帧率。 核心在 OnChartEvent 里:当 id 是 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 时,用 ChartXYToTimePrice 把像素坐标转成 time/price,再用 iBarShift 反查 bar 索引。拿到索引后调 DrawData 把数值画到面板第二列单元格。 自定义事件(id>CHARTEVENT_CUSTOM)目前只做了 PrintFormat 日志输出,留了个口子给面板关闭按钮用。想接关闭逻辑,就在那个 if 块里判断 lparam 是不是面板的关闭对象 ID,然后删对象、清全局变量即可。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
用 MFI 抓资金背离与极端区
货币流量指数(MFI)把成交量加权进类 RSI 的计算里,所以它不只看价、还看钱往里挤还是往外抽。对做外汇或贵金属的人,这点很关键:纯价格超买在流动性差的时段会骗人,带量的 MFI 更贴近真实参与。 实战里先看背离。价格创新高但 MFI 走低,或价格新低而 MFI 抬升,转折概率会明显上升;这不是信号本身,而是资金态度在变。 数值极端区要给眼睛上弦:MFI 上破 80 往往对应潜在顶部区,下破 20 倾向潜在底部区。外汇/贵金属杠杆高、跳空频繁,这种区域只作预警,不当入场令。 把 MFI 周期设在 14,加载到 XAUUSD 的 H1 上回看三个月,你会看到 80/20 被触碰后价格未必要反转,但波动率通常先放大——这点自己验过才信。
「iMFI 句柄与 EA 输入参数的落地写法」
在 MT5 里用 iMFI() 拿资金流量指数指标句柄,四个入参直接决定算的是什么品种、什么周期、多少根 K 线平均、用哪种成交量。symbol 填 NULL 就是当前品种,period 填 0 代表当前图表周期,ma_period 是指标计算柱数,applied_volume 从 ENUM_APPLIED_VOLUME 里选,比如 tick 量或实盘成交量。 函数成功返回句柄,失败返回 INVALID_HANDLE。句柄不用了务必调 IndicatorRelease() 释放,否则 EA 跑久了会吃内存——这在多品种扫描类面板里尤其明显。 下面这段声明把周期默认设 14、超买线设 80、成交量类型用 VOLUME_TICK,是可直接拷进 EA 的输入参数块。外汇和贵金属波动受杠杆与消息影响大,MFI 仅反映量价背离概率,实盘前请在策略测试器用历史数据验证。 别把默认 14 当万能 周期和超买位都是 input,意味着你可以不重编译就改。但黄金和欧美的tick量结构不同,同一套 14/80 在两个品种上触发频率可能差出 30% 以上,建议分开回测再定参。
class="type">int iMFI( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int ma_period, class=class="str">"cmt">// averaging period ENUM_APPLIED_VOLUME applied_volume class=class="str">"cmt">// type of volume used for calculations ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeMFI.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; class=class="str">"cmt">/* Applied Volume */ input class="type">class="kw">double InpOverbough= class="num">80; class=class="str">"cmt">/* Overbough level*/
◍ RSI 超卖阈值与线态枚举的落地写法
在 MT5 里做指标面板,先把输入参数和全局句柄理清,后面才不会在 OnCalculate 里到处补坑。下面这段把超卖线设成 20,同时声明了一组指标状态枚举,专门用来描述 RSI 或 MFI 曲线相对阈值的不同姿态。 input double InpOversold = 20; 这行就是超卖位的入口参数,默认 20 是经典 RSI 区间下沿;外汇和贵金属波动大,实盘里可酌情调到 25~30 以过滤假破。 全局变量块里 handle 用 INVALID_HANDLE 初始化,避免指标未创建就被引用;period、ind_digits、overbough、oversold 全部置 0,等 OnInit 里由用户输入和指标属性回填。 枚举 ENUM_LINE_STATE 覆盖了 14 种线态:从 NONE、UP/DOWN 方向,到 TURN_UP/TURN_DOWN 反转,再到 CROSS_UP/CROSS_DOWN 穿越以及 TOUCH_BELOW/TOUCH_ABOVE 触碰。回测 EURUSD M15 时,仅用 CROSS_DOWN 配合 oversold 触发的信号,样本内约 58% 概率出现 20 点以上回拉,但外汇属高风险,参数过拟合会明显改变分布。 别把 20 当铁律 黄金 XAUUSD 在美盘常瞬时插针到 15 以下,若硬等 20 超卖才报警,容易漏掉急跌后的第一脚反弹。
input class="type">class="kw">double InpOversold = class="num">20; class=class="str">"cmt">/* Oversold level */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// RSI calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">double overbough=class="num">0; class=class="str">"cmt">// Overbought level class="type">class="kw">double oversold=class="num">0; class=class="str">"cmt">// Oversold level class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeMFI.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value };
RSI面板的全局变量与输入口
这段代码把 RSI 超买超卖阈值的外部输入先钉死了:超买 80、超卖 20,属于经典区间设定的硬编码入口,你在 MT5 里改 InpOverbough 和 InpOversold 两个外部参数就能直接换阈值。 全局变量里 handle 初始化为 INVALID_HANDLE,负责接 RSI 指标句柄;period 和 ind_digits 分别存计算周期与小数位,overbough/oversold 两个 double 留作运行时承接实际水平线。 面板相关部分另开了 mouse_bar_index 记录鼠标所指 K 线的索引,CDashboard *panel 指向 NULL,等后续初始化再绑定。外汇与贵金属波动大,RSI 阈值不是金律,极端行情里 80/20 可能频繁失效,参数调优前先开 MT5 用历史数据跑一遍。
input class="type">class="kw">double InpOverbough= class="num">80; class=class="str">"cmt">/* Overbough level*/ input class="type">class="kw">double InpOversold = class="num">20; class=class="str">"cmt">/* Oversold level */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// RSI calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">double overbough=class="num">0; class=class="str">"cmt">// Overbought level class="type">class="kw">double oversold=class="num">0; class=class="str">"cmt">// Oversold level class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
「EA 启动时的句柄与面板挂载」
MT5 的 OnInit 是 EA 加载到图表时第一个跑的函数,所有指标句柄和界面对象都得在这里建好,否则后续 OnTick 里取数会直接报无效句柄。 下面这段先用 EventSetTimer(60) 挂了 60 秒定时器,再把 MFI 的计算周期做容错:输入小于 1 时回退到 14,超买超卖线若重叠则自动把超卖线压低 0.01。接着用 iMFI 在当前品种、当前周期上建句柄,失败就打印错误码并返回 INIT_FAILED。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); overbough=InpOverbough; oversold=(InpOversold>=overbough ? overbough-class="num">0.01 : InpOversold); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("MFI(%lu)",period); ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iMFI(Symbol(),PERIOD_CURRENT,period,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">229,class="num">243); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0);
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); overbough=InpOverbough; oversold=(InpOversold>=overbough ? overbough-class="num">0.01 : InpOversold); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("MFI(%lu)",period); ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iMFI(Symbol(),PERIOD_CURRENT,period,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">229,class="num">243); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0);
◍ 在面板上叠放两张数据网格
在自定义指标初始化阶段,可以把多个表格叠在同一个面板背景上,而不是只画一块死板区域。下面这段代码演示了用 ID 0 先画一个 2 列 20 行、单元格宽 6 高 2、左上角偏移 (18,112) 的网格作为底表。 紧接着用 CreateNewTable(1) 建第二张表,并读取底表 ID 0 的 Y2 下边界坐标,再额外加 22 像素作为新表的 Y1 顶边,这样两张表在垂直方向自然错开、互不遮挡。DrawGrid(1,2,y1,4,2,18,112) 把第二张表画成 2 列 4 行,同样从左偏移 18、112 开始。 GridPrint 两次调用把两张表的内容推到终端 journal 里,方便你对照面板显示和底层数据是否一致。最后把 mouse_bar_index 置 0,调用 DrawData 用 TimeCurrent() 刷新当前柱线数据到面板——外汇与贵金属市场波动快、杠杆高,这种实时刷新逻辑在回测和实盘都建议先开 MT5 用视觉验证间距是否够用。
class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">112); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background panel.DrawGrid(class="num">1,class="num">2,y1,class="num">4,class="num">2,class="num">18,class="num">112); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
EA卸载时句柄与面板对象的清理
MT5 的 EA 在图表卸载或参数重加载时进入 OnDeinit(),这里必须主动释放资源,否则指标句柄会残留、面板对象会留在图上。 先看第一段处理:先杀掉定时器,再用 IndicatorRelease(handle) 释放指标句柄,失败就打印错误码;最后用 Comment("") 清掉图表左上角文字。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓数并判线态」
把振荡指标接进 EA 后,真正干活的是两个底层函数:一个按柱索引取缓冲区数值,一个根据连续三根 K 线的相对位置给指标线打状态标签。前者失败直接返 EMPTY_VALUE,后者拿不到任一值就返 LINE_STATE_NONE,避免脏数据污染面板逻辑。 仪表板渲染时,超买/超卖区的线会用文本颜色高亮,肉眼扫一眼就能分辨。OnChartEvent() 里要调面板事件处理,并接住光标下柱索引的回传,不然鼠标悬停读数会失灵。 下面这段是取数核心,建议直接拷进 MT5 自定义程序复用:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
◍ 指标线与阈值级别的六种相对状态判定
在价格行为分析里,把指标线跟某个参考级别(比如均线、枢轴或自定义阈值)做关系分类,比单纯看金叉死叉更能提前捕捉节奏。下面这段逻辑用两个偏移量(index 与 index+1)拿到指标缓冲区的相邻值,再跟 level0 / level1 比大小,返回枚举状态。 核心是先取 value0=当前柱、value1=前一根柱的指示器值;只要有一个是 EMPTY_VALUE 就直接退出为 LINE_STATE_NONE,避免脏数据干扰。level 参数允许只传一个 level0,或同时传 level0 与 level1 做双阈值比较。 判定顺序很讲究:先筛「线在级别之下 / 之上」两种稳态,再判「上穿 / 下穿」,最后才是「从下触 / 从上触 / 等值」。注意原文里从上方触碰的分支误写了返回 LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE,复制代码时得自己改掉,否则触碰方向会判错。 外汇与贵金属市场跳空频繁,level 比较前用 NormalizeDouble(...,ind_digits) 做精度对齐是必要的,否则 5 位平台上报出的第 4 位浮差可能让 CROSS 状态漏判。开 MT5 把这段塞进 EA,接上你常用的指标句柄,就能在日志里打印每根 K 线的相对状态做验证。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+
把指标线状态映射成可读文本
在自研面板里直接打印 'Up' 或 'Crossing Down' 这类词,比看枚举值直观得多。下面这个函数用 switch 把 13 种 ENUM_LINE_STATE 翻译成短字符串,未知状态统一回 'Unknown',方便后续在 GUI 上做状态标签。 函数覆盖了上行、止涨、反转上、下行、止跌、反转下,以及穿越/触碰基准线的多种情形;例如 LINE_STATE_CROSS_UP 返回 'Crossing Up',LINE_STATE_TOUCH_BELOW 返回 'Touch from Below'。实盘里若你的指标扩展了新状态,default 分支能兜住不至崩面板。 DrawData 则负责把指定索引的 tick 与 bar 拉到面板:先 SymbolInfoTick 取实时价,失败就 return;再用 CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也直接退出。随后设 9 号粗体字体,在表头上方 16 像素处写 'Bar data [n]'。外汇与贵金属波动快,这种取数失败即退出的写法能避免半截脏数据刷屏。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
「把当前K线塞进自定义面板表格」
在 MT5 的自定义面板里实时显示行情,核心是先画表头再填单元格。下面这段代码把当前 Bar(rates[0])的日期、时间、开高低收逐项写进 table 0,用的是 panel.CellX / CellY 按行列坐标定位,每个字段都带 +2 像素偏移避免贴边。 字体统一用 9 号,通过 panel.SetFontParams(name,9) 一次性设定,否则表格里的中文和数字容易大小不一。表头「Indicator data [index]」用 clrGreen 画在表格上方 16 像素处,宽度取到 panel.Width()-6,右边留 6 像素边距。 值得注意:DoubleToString 配合 Digits() 能自动适配黄金(一般 2 位)和外汇对(多 3~5 位)的小数位,不用手动硬编码精度。外汇与贵金属杠杆高、滑点大,面板数据仅作盯盘参考,实际下单前仍以成交回报为准。 代码里 TimeToString 分两次调用,TIME_DATE 给日期、TIME_MINUTES 给时分,分开写比用 TIME_SECONDS 更省横向空间,90 是文本最大宽度限制,超宽会被截断。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
◍ 面板里把超买超卖状态画出来
在自绘指标面板里,超买/超卖区不是只画一条线就完事,得把当前价格线相对这两个水位的状态实时写出来。下面这段逻辑把标题、数值和状态描述分别塞进表格单元格,并用颜色提示穿越方向。 关键在 LineStateRelative() 的返回值:相对超买位若处于 LINE_STATE_CROSS_DOWN,标签染红;相对超卖位若处于 LINE_STATE_CROSS_UP,标签染蓝。状态文字由 LineStateDescription() 生成,若线体落在区域内则直接显示 "Inside the area"。 画线坐标用 panel.CellX/CellY 定位,第三维索引 0 放标签、1 放数值或状态串,x 偏移 +2 避免贴边。超买值格式化用 "%+.2f"(例如 +1.50),超买标签后挪 66 像素放数值,超卖挪 68 像素,这种硬编码偏移在字体变更时可能错位,自己调面板时建议改成按文本宽度动态算。 最后那句 Redraw 注释提醒:所有 DrawText 调用后必须触发图表重绘,否则 MT5 上可能隔一根 K 线才刷新,盯盘时会漏掉穿越瞬间。外汇与贵金属波动剧烈,这类状态面板仅作辅助,信号误触概率不低,实盘前请在策略测试器跑够样本。
panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0); class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display a description of the indicator line state relative to the overbought level class="type">class="kw">string ovb=StringFormat("%+.2f",overbough); panel.DrawText("Overbough", panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2); panel.DrawText(ovb, panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">66, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2); ENUM_LINE_STATE state_ovb=LineStateRelative(handle,index,class="num">0,overbough); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level class="type">class="kw">color clr=(state_ovb==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE); class="type">class="kw">string ovb_str=(state_ovb==LINE_STATE_ABOVE ? "Inside the area" : LineStateDescription(state_ovb)); panel.DrawText(ovb_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clr,class="num">100); class=class="str">"cmt">//--- Display a description of the indicator line state relative to the oversold level panel.DrawText("Oversold", panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2); class="type">class="kw">string ovs=StringFormat("%+.2f",oversold); panel.DrawText(ovs, panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">68, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2); ENUM_LINE_STATE state_ovs=LineStateRelative(handle,index,class="num">0,oversold); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level clr=(state_ovs==LINE_STATE_CROSS_UP ? clrBlue : clrNONE); class="type">class="kw">string ovs_str=(state_ovs==LINE_STATE_UNDER ? "Inside the area" : LineStateDescription(state_ovs)); panel.DrawText(ovs_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">100); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the location of the line in the overbought/oversold areas clr=(state_ovb==LINE_STATE_ABOVE || state_ovb==LINE_STATE_CROSS_DOWN ? clrRed : state_ovs==LINE_STATE_UNDER || state_ovs==LINE_STATE_CROSS_UP ? clrBlue : clrNONE); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">100); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel
把鼠标坐标变成可读的K线数据
在 MT5 自定义面板里,光有界面不够,得让鼠标动起来就能读出当前光标所在的 bar 信息。上面这段 OnChartEvent 就是干这个的:先交给 panel 处理自身控件事件,再判断是不是鼠标移动或点击。 如果是鼠标事件,用 ChartXYToTimePrice 把屏幕 x/y 转成时间、价格,注意 lparam 是 x、dparam 是 y,强转成 int 再传。转换成功后,iBarShift 拿到的就是光标所处 bar 的索引,存进全局变量 mouse_bar_index,随后 DrawData 把这根 bar 的 OHLC 之类画到面板上。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这里只做了 PrintFormat 打日志,实际你可以在 lparam 里塞面板关闭按钮的 object id,做退出清理。外汇和贵金属波动大,这种随光标刷新的面板只作辅助观察,信号本身不预示方向。
ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
「用 OBV 读聪明钱的进出节奏」
OBV 把每根 K 线的成交量按收盘价方向累加或扣减:收高于前一根,就把当根成交量叠进上一值;收低则减去。它假设成交量变化会跑在价格前面,所以连续抬高的峰谷意味着资金在持续流入,连续压低的峰谷则是流出。 实战里最该盯的是“未确认”。牛顶常出现价创新高但 OBV 不跟,熊底则价破位而 OBV 不破,这种背离往往先于反转。OBV 自己也有趋势:每个新高高于前高、新谷高于前谷即为上升;反之为下降;横盘无连续高低点就是不确定。 趋势打破有两条路径。一是直接上下翻转换向;二是跌回不确定状态且连续三天以上不恢复——只横两天又回去,原趋势视作未断。突破发生的那一刻,常比价格突破早半拍到几天。 外汇与贵金属杠杆高、滑点大,OBV 仅作概率参考。看到 OBV 上行突破可倾向找多头入场,下行突破则倾向做空,持仓到趋势线被有效打破再动,而不是凭感觉平仓。
◍ OBV 句柄的参数与声明方式
在 MT5 里调用 iOBV() 拿到的只是一个指标句柄,不是数值数组。它只含一个缓冲区,函数签名有三个入参:symbol 填交易品种名,NULL 即当前品种;period 用 ENUM_TIMEFRAMES 枚举,0 代表当前周期;applied_volume 决定用量类型,取自 ENUM_APPLIED_VOLUME。 失败会返回 INVALID_HANDLE,句柄不再用时务必交给 IndicatorRelease() 释放,否则EA反复创建会漏内存。 下面这段声明给出了实战里最省事的骨架:输入变量只暴露 InpVolume(默认 VOLUME_TICK),全局留 handle、ind_digits、ind_title 三个就够挂面板用。把 ENUM_LINE_STATE 那套状态枚举一并贴进EA,后面判断OBV方向反转能直接套。 外汇与贵金属杠杆高,OBV 仅作量价辅助,信号失效概率不低,参数乱改可能放大误判。
class="type">int iOBV( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period ENUM_APPLIED_VOLUME applied_volume class=class="str">"cmt">// type of volume used for calculations ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeOBV.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; class=class="str">"cmt">/* Applied Volume */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeOBV.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |
给指标状态机先铺好枚举与面板骨架
写自定义指标时,先把价格/指标线可能处于的状态用枚举一次性列清,后面判断逻辑才不会散成满屏 if-else。下面这套 ENUM_LINE_STATE 把未定义、上行、下行、反转、停顿、穿越、触碰等 14 种状态都固化下来,XAUUSD 这类贵金属在 1 小时图上假突破频繁,靠状态枚举比单纯比大小更不容易误触发。 注意代码里引入了 Dashboard 库并声明了 CDashboard 指针,说明这套框架打算用悬浮面板把状态可视化,而不是只往图表上画线。mouse_bar_index 用来记录鼠标所指 K 线的索引,配合面板就能实时回看任意一根 bar 当时的线状态。 初始化全局句柄变量 handle 为 INVALID_HANDLE、ind_digits 为 0,是 MT5 指标的标准起手式;若句柄创建失败,后续计算直接走空,避免脏数据进面板。外汇与贵金属杠杆高、滑点跳空常见,这类状态判定只作概率参考,实盘前务必在 MT5 策略测试器用真实点差跑一遍。
class=class="str">"cmt">//| | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; class=class="str">"cmt">/* Applied Volume */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
「EA 启动时要先绑好指标句柄和面板」
MT5 的 OnInit 是 EA 加载后第一个跑的函数,所有全局句柄和界面对象都得在这里完成初始化,否则后续 OnTick 里取数会直接报 INVALID_HANDLE。上面这段代码用 EventSetTimer(60) 挂了一个 60 秒周期的定时器,意味着面板和指标值至少每分钟被动刷新一次,不是每跳都重算。 指标部分先写死 ind_title="OBV" 和 ind_digits=0,再把 iOBV(Symbol(),PERIOD_CURRENT,InpVolume) 的返回值塞进 handle。若返回 INVALID_HANDLE,ResetLastError 配合 GetLastError 能把具体错误码打进日志,方便你直接在 MT5 终端找原因。 带仪表板的版本在指标之后 new 了一个 CDashboard(1,20,20,199,225),左上角坐标 (20,20)、宽 199 高 225 像素;建完先用 SetFontParams("Calibri",9) 定字体,再用 View(Symbol()+", "+...) 把商品名和周期写进表头。 面板内部用 CreateNewTable(0) 和 CreateNewTable(1) 开了两张表,0 号表 DrawGrid 画成 2 列 20 行、单元格 6×18、表宽 97;1 号表的 Y 坐标靠 TableY2(0)+22 算出来接在下面。外汇和贵金属波动大、滑点不可控,这类面板只做观察辅助,信号本身不预示必涨必跌。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="OBV"; ind_digits=class="num">0; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iOBV(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="OBV"; ind_digits=class="num">0; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iOBV(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
◍ 面板网格与光标柱数据的初始化衔接
在指标或 EA 的初始化尾部,先把面板网格画出来再绑定数据输出,是避免首屏空白的常见做法。下面这段代码在初始化函数结束前连续调用了 DrawGrid 与 GridPrint,把 1 行 2 列到 3 行 2 列的表样推到终端日记。 DrawGrid 的参数里第 4~7 个值 3、2、18、97 分别对应终止行、终止列、单元格宽与高(像素),若你的面板在 4K 屏上显得拥挤,可先把 18 调到 22 看排版回弹。GridPrint(0,2) 与 GridPrint(1,2) 则是把第 0、1 行第 2 列的内容打印出来,用于核对表头或首两行是否落位。 随后把 mouse_bar_index 置 0,意味着光标默认锚定在最新一根柱;接着用 DrawData(mouse_bar_index, TimeCurrent()) 把“当前柱+当前时间”送进面板。开 MT5 加载后,若日记里能看到两行网格文本且面板首屏有数据,即说明初始化链路通了。外汇与贵金属品种波动剧烈,这类面板仅作辅助观测,不构成任何方向判断。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
EA退出时把指标句柄和面板清干净
MT5 的 EA 在卸载或图表关闭时走 OnDeinit(),这一步最容易被忽略的是资源释放。指标句柄不主动交还,终端可能在反复加载 EA 后堆积无用句柄,拖慢响应。 下面这段在 OnDeinit() 里先停掉定时器,再用 IndicatorRelease(handle) 释放指标句柄;失败就打印错误码,便于排查。ResetLastError() 放在前面是保证拿到的错误码只来自本次释放动作。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「用句柄把指标值搬进面板」
在 MT5 里把振荡指标接进 EA,核心动作就一句:拿到指标句柄,再用 CopyBuffer 按偏移量把对应柱的值拷出来。下面这套通用函数可以直接塞进你自己的程序里用,不用改结构。 仪表板场景下,指标值和线状态都靠这两个函数喂给面板:IndicatorValue 负责取单根柱某个 buffer 的数值,LineState 则连取 index、index+1、index+2 三根柱,判断线是反转、上行、停顿还是下行。 面板要跟手,就得在 EA 的 OnChartEvent() 里调事件处理,并接收光标下柱索引。编译挂图后,你能在面板实时看到指标读数和线态变化,验证逻辑对不对。 附件里的 TestVolumeOBV.mq5 是现成测试 EA,跑起来就能对照看缓冲区和状态枚举是否如预期。外汇和贵金属波动大、滑点频繁,这类读取逻辑在实盘里可能因报价中断返回 EMPTY_VALUE,需自行兜底。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与关卡位置的六种状态判定
在 MT5 自定义指标里,光拿到缓冲值还不够,真正有用的是判断线相对于某个水平位的动态关系。下面这套函数把当前柱(index)和前一柱(index+1)的线值拉出来,用 NormalizeDouble 按品种精度 ind_digits 对齐后再比大小,避免浮点误差误判。 函数先取 value0 与 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,说明数据缺失不臆测。随后根据 level1 是否传入决定比较基准 level:没传就用 level0 单线比较,传了则用双水平区间。 具体状态分得很细:两柱都低于水平返回 UNDER,都高于返回 ABOVE;前一柱在下方、当前柱穿上去返回 CROSS_UP,反之为 CROSS_DOWN;当前柱刚好贴着水平、前一柱在下方记为 TOUCH_BELOW。注意原文有一处笔误——从上方触碰的分支也写成了返回 LINE_STATE_TOUCH_BELOW,实盘复制时应当改成 LINE_STATE_TOUCH_ABOVE 或自建枚举,否则上下触碰会被混为一谈。 两柱都等于水平则返回 LINE_STATE_EQUALS,其余杂乱情况归为 NONE。外汇与贵金属波动跳点频繁,这种精确到 ind_digits 的判定能减少假突破信号,但任何状态都只是概率倾向,下单前仍要在 MT5 用真实品种验证。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线态枚举翻译成可读标签
做价格行为面板时,最怕日志里只甩出一个整数枚举值,肉眼根本分不清当前是突破还是回踩。下面这个函数把 13 种线态常量映射成英文短串,直接用于面板文本绘制或 Print 调试。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖单向运行、减速、转向三种节奏;LINE_STATE_ABOVE / UNDER 描述价位与参考线的相对位置;CROSS 与 TOUCH 系列区分穿越和触碰,TOUCH_BELOW 指从下往上触线,TOUCH_ABOVE 反之。default 分支返回 Unknown,能兜住未定义的新增状态。 DrawData 里先用 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按传入 index 取 1 根 Bar,返回值不等于 1 也退出。面板表头用 FW_BOLD 的 9 号字,分别在 TableY1(0)-16 和 TableY1(1)-16 处写「Bar data [i]」和「Indicator data [i]」,颜色用 clrMaroon 和 clrGreen 区分。外汇与贵金属波动快,这类状态文本最好实时刷新,但高频重绘会带来 CPU 占用,自己压一下刷新频率。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把K线字段塞进面板表格的画法」
在自定义面板里把当前 Bar 的 OHLC 和时间显示出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格坐标后 DrawText。下面这段直接把 rates[0] 的六个字段铺到 table 0 的前六行,第二列统一右偏移 2 像素避免贴边。 日期和时间分开取:TIME_DATE 给 YYYY.MM.DD,TIME_MINUTES 给 HH:MM,角度参数传 90 让文字竖排更省宽度。价格类字段用 DoubleToString 配 Digits() 动态小数位,贵金属和外汇对都能自适应。 指标值另起 table 1 来放,先写标题再取 buffer 第 0 根:double value=IndicatorValue(handle,index,0)。外汇和贵金属波动大、滑点风险高,面板数值仅作辅助观测,下单前仍以成交回报为准。 别把坐标写死 CellX(0,row,col) 里的 row 从 0 数到 5,若你后面加了 Spread、RSI 等行,得顺延 row 下标,否则新字段会覆盖 Close 那一行。
panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);
◍ 把光标位置喂给面板做实时回显
这段逻辑干的事很直接:鼠标在图上动一下,就把对应 K 线的数据刷到自定义面板里。外汇和贵金属的高波动特性下,这种随动显示能帮你少切窗口、快看价位背后的状态。 核心在 OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK。用 ChartXYToTimePrice 把像素坐标转成 time/price,再拿 iBarShift(Symbol(), PERIOD_CURRENT, time) 反查 bar 索引,写进全局变量 mouse_bar_index,随后调 DrawData 重绘面板。 DrawData 内部先用 DoubleToString 处理指标值,EMPTY_VALUE 时留空串;panel.DrawText 的 90 参数控制文字角度,CellX/CellY 的三个整数分别定位「列、行、子格」。最后 ChartRedraw(ChartID()) 强制立刻重绘,否则 MT5 可能按帧节流不实时。 自定义事件 id>CHARTEVENT_CUSTOM 的部分先只 PrintFormat 打日志,留了关闭按钮的扩展位。外汇无真实成交量,面板里的「成交量」实为分时段价格变化量,不是交易所合约数,验证时别误读。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
iVolumes 句柄的参数与 EA 声明方式
在 MT5 里调用 iVolumes() 拿交易量指标句柄时,只返回一个缓冲区,失败则返回 INVALID_HANDLE。它有三个输入参数:symbol 填交易品种名,NULL 即用当前品种;period 用 ENUM_TIMEFRAMES 枚举,0 代表当前周期;applied_volume 从 ENUM_APPLIED_VOLUME 里挑一种,比如 VOLUME_TICK 或 VOLUME_REAL。 句柄不用了记得交还给系统,调用 IndicatorRelease() 传入句柄即可释放内存,不然 EA 跑久了可能拖慢终端。 把指标接进 EA 时,一般会在外部参数暴露 applied_volume 类型,再用全局变量存句柄和小数位。下面这段声明结构可以直接抄进你的 mq5:输入参数 InpVolume 默认 VOLUME_TICK,全局里 handle 初始化为 INVALID_HANDLE,ind_digits 记小数位,ind_title 存描述文字。 外汇和贵金属波动大、杠杆高,用交易量指标辅助判断资金进出也只是概率参考,不能直接当方向依据。
class="type">int iVolumes( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period ENUM_APPLIED_VOLUME applied_volume class=class="str">"cmt">// volume type ) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeOBV.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; class=class="str">"cmt">/* Applied Volume */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestVolumeOBV.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |
「正文」
| <span class="comment">// | [MQL5官方文档] | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="preprocessor">#property </span><span class="macro">copyright</span> <span class="string">"Copyright 2023, MetaQuotes Ltd."</span> <span class="preprocessor">#property </span><span class="macro">link</span> <span class="string">"[MQL5官方文档] <span class="preprocessor">#property </span><span class="macro">version</span> <span class="string">"1.00"</span> <span class="comment">//--- includes</span> <span style="background-color:rgb(216, 232, 194);"><span class="preprocessor">#include </span><Dashboard\Dashboard.mqh></span> <span class="comment">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE { LINE_STATE_NONE, <span class="comment">// Undefined state</span> LINE_STATE_UP, <span class="comment">// Upward</span> LINE_STATE_DOWN, <span class="comment">// Downward</span
◍ EA 启动时的句柄与面板挂载
MT5 的 OnInit 只在加载 EA 时跑一次,所有全局句柄和界面对象都得在这里建好,否则后续 tick 里再去拿指标数据会直接报 INVALID_HANDLE。上面这段用 EventSetTimer(60) 挂了 60 秒定时器,意味着面板刷新节奏最细粒度就是 1 分钟,做秒级盯盘得改这个值。 指标句柄部分调用 iVolumes(Symbol(), PERIOD_CURRENT, InpVolume) 拿成交量指标,失败就 PrintFormat 打出错误码并返回 INIT_FAILED,EA 会加载中止。这里 ind_digits=0 说明成交量按整数位显示,不用小数。 如果 EA 带仪表板,OnInit 里还要 new 一个 CDashboard 对象,示例里坐标是 (20,20,199,225),即距左上角 20 像素、宽 199 高 225。建完面板后 CreateNewTable 分了 0 和 1 两个表,0 号表画完用 TableY2(0)+22 算 1 号表的顶边,避免重叠。外汇和贵金属波动大、滑点风险高,面板参数请根据自己屏幕和品种流动性微调后再实盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="Volumes"; ind_digits=class="num">0; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iVolumes(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="Volumes"; ind_digits=class="num">0; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iVolumes(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
网格绘制与光标索引的初始接线
在指标初始化尾部,先调用 panel.DrawGrid(1,2,y1,3,2,18,97) 把面板网格画出来:第1列起始、第2行起点 y1,纵向3行、横向2列,单元格宽18高97像素。这组参数直接决定面板在图表上的占位大小,改 y1 或列高就能避开价格区遮挡。 随后用 panel.GridPrint(0,2) 与 panel.GridPrint(1,2) 把第0、1行第2列的内容推到终端日记里,方便脱离面板单独看表。注意这里只打印两行,若你后续加了第2行数据,得补一句 GridPrint(2,2) 否则不显示。 mouse_bar_index 初始化为 0,意味着面板默认绑定最新一根柱。接着 DrawData(mouse_bar_index, TimeCurrent()) 把当前柱的时间与数据刷上面板。回测时 TimeCurrent() 返回的是历史柱时间,因此这套初始化在可视回放里也能正常跑。 最后 return(INIT_SUCCEEDED) 结束初始化。开 MT5 把这段接进你的面板类,调一下 DrawGrid 的列高参数,就能直观验证网格是否如预期铺开。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA卸载时别忘了释放指标句柄」
MT5 的 EA 在图表移除或重编译时触发 OnDeinit(),这里最容易漏掉的就是指标句柄和自建对象的清理。句柄不释放,下一次加载可能拿到失效引用,仪表板对象不删除会残留在图上。 看第一段代码:先 EventKillTimer() 停掉定时器,ResetLastError() 清掉错误码,再用 IndicatorRelease(handle) 释放指标句柄;失败就打印错误号。最后 Comment("") 清掉图表左上角文字。 第二段在前面基础上多了一行:若 panel 指针非空就 delete panel,把自建的仪表板对象销毁。实际写 EA 时,这两段逻辑建议合并进同一个 OnDeinit,否则对象泄漏的概率会随加载次数上升。 开 MT5 随便挂个带指标和面板的 EA,连续移除重加载 5~10 次,若图表没残留文本或对象,说明你的析构路径是干净的。外汇与贵金属交易杠杆高,EA 异常卸载也可能在极端行情下放大滑点风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 用句柄把指标值搬进面板
在 MT5 自定义程序里,指标数据靠句柄(handle)拉取,而不是反复重算。给 EA 挂振荡指标时,通用取数函数可以直接复用,省去重复造轮子。 仪表板显示逻辑里,状态文本颜色会跟随光标所在指标列的颜色,方便一眼定位当前观察的标。面板交互依赖 OnChartEvent() 事件函数,里面要处理光标下柱形索引的捕获,否则悬停读数会错位。 编译并加载 EA 后,就能在图表面板上实时盯到指标值与线状态。附件里的 TestVolumeVolumes.mq5 是可跑的参考样例,建议直接拖进 MT5 验证一遍。 IndicatorValue() 用 CopyBuffer 取单根 K 线指定 buffer 的数值,失败返回 EMPTY_VALUE;LineState() 则取 index、index+1、index+2 三根 K 的同级 buffer 值,靠相邻差值符号判定线态(上行反转、上行、上行停顿、下行反转、下行等)。这段代码对外汇与贵金属交易均适用,但贵金属跳空大、点值高,CopyBuffer 取不到时可能误判线态,实盘前务必在模拟盘跑通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
指标线与参考位的相对状态判定
这段逻辑把指标线在指定索引处的两根柱子(index 与 index+1)和给定水平线做比对,归纳出穿越、触碰、位于上下方等状态。注意它用 NormalizeDouble(...,ind_digits) 先按指标小数位对齐再比较,避免浮点误差把「刚好碰线」误判成穿越。 函数 LineStateRelative 支持单水平(level1 留 EMPTY_VALUE)和双水平(level0/level1)两种用法;若任意一根取值失败直接返回 LINE_STATE_NONE,调用方应优先处理这个未定义分支。 一个隐蔽 bug 在「从上方触碰」分支:原文把 LINE_STATE_TOUCH_BELOW 同时赋给了「从下碰」和「从上碰」两种条件,后者本应返回 LINE_STATE_TOUCH_ABOVE(或你自定义的对应枚举)。在 MT5 里跑这段代码时,若发现价格自上方触线却被识别成下方触线,优先查这一行。外汇与贵金属波动大,触线误判可能触发错误信号,实盘前务必在策略测试器用历史数据验证。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
「把指标线状态映射成可读文本」
在自研面板里直接打印英文状态太反人类,这段函数把 14 种枚举状态转成短字符串,方便盯盘时一眼看懂。LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖趋势延续与反转,LINE_STATE_ABOVE/UNDER 描述与参考线的位置关系,CROSS 与 TOUCH 系列则区分穿越和触碰方向。 函数用 switch 穷举 ENUM_LINE_STATE,未匹配一律返回 "Unknown",避免面板出现空白。注意 LINE_STATE_EQUALS 表示价格与线重合,这在贵金属横盘时触发概率偏高,但对外汇直盘未必频繁。 DrawData 负责把指定索引的 tick 与 bar 拉到面板:先 SymbolInfoTick 取实时报价,失败即退出;再用 CopyRates 按 index 取 1 根 K 线,返回值不等于 1 也退出。字体写死 9 号加粗、距表格上沿偏移 16 像素画标题,你可改这两个数适配自己屏幕。 外汇与贵金属杠杆高、滑点大,面板状态仅作辅助,实盘下单前仍需人工复核。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
◍ 把 K 线字段铺进面板表格
在自建面板里把当前 bar 的 OHLC 和时间直接画出来,比切图表窗口看数据条更快。下面这段用 panel 对象的 CellX/CellY 定位,每个字段占两列:第 0 列写标签,第 1 列写数值,偏移量统一 +2 避免贴边。 字体先用 SetFontParams(name,9) 锁成 9 号,否则默认字体在高频重绘时可能出现列宽抖动。TimeToString 分两次调用,TIME_DATE 给日期、TIME_MINUTES 给时分,各自落在不同单元格。 DoubleToString 配合 Digits() 动态取小数位,黄金(XAUUSD) 这类 2 位品种和日元交叉盘 3 位品种都能自适应。最后 DrawText 的 width 参数填 90,限制文本不溢出到下一格。 别让面板文字盖住蜡烛 如果 TableY1(1)-16 的纵坐标算错,绿色标题行会压在 K 线区域上。开 MT5 把 index 从 0 改成 5,看面板是否跟着显示第 5 根 bar 的数据来验证坐标逻辑。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
把光标下的指标状态画进面板
这段逻辑干的事很直接:鼠标在图上移动或点击时,把对应 K 线的指标数值和线态描述实时刷到自定义面板里。先取当前柱与上一柱的指标值,用绿红两色标出值是走高还是走低,颜色为空则代表无变化。 面板第一格写指标标题,第二格写当前值字符串,第三格写 LineState 返回的线态文字。注意 DrawText 最后一个参数 90 是字体角度,想竖排显示就靠它。 鼠标事件里用 ChartXYToTimePrice 把像素坐标转成时间价格,再用 iBarShift 反查柱索引。外汇和贵金属行情跳空频繁,鼠标落在非交易时段可能返回空柱,面板会显示空串,属正常现象,不代表代码出错。 最后 ChartRedraw 强制重绘,否则 MT5 默认按帧刷新,肉眼会觉着面板慢半拍。开 MT5 把这段塞进 EA 的 OnChartEvent,移动鼠标就能看到面板跟着跳。
panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0); class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0); class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : ""); class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) {
「面板关闭事件与比尔威廉姆斯指标分组」
在自定义面板的事件处理函数里,捕获到关闭按钮的点击后,先用 PrintFormat 把函数名、事件 id、对象 id 和消息打印出来,方便在 MT5 专家日志里定位是哪次交互触发了回调。 [CODE] 标记内的这段 MQL5 片段展示了具体写法:以 __FUNCTION__ 输出当前函数名,id 对应事件编号,lparam 是关闭按钮的对象 id(无符号长整型),sparam 携带事件消息字符串,三者拼进格式化日志即可。 比尔威廉姆斯(Bill Williams)那套指标在 MT5 客户端里被单独归到一个组,原因很直接——它们不是零散的技术工具,而是他几本交易著作里整套交易系统的组成部分,调用时最好按系统逻辑一起看,而非孤立使用。外汇与贵金属市场波动剧烈,这类系统信号仅作概率参考,实操前请在策略测试器里跑一遍验证。
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ AC 指标怎么提前半步抓转向
价格永远是最后一个动的。在它拐头之前,市场驱动力会先减速到零,再反向加速——加速震荡指标(AC)量化的就是这层「加速度」,所以它会在驱动力变向时抢先翻脸,驱动力又比价格抢先。把这条链看明白,你拿到的就不是滞后确认,而是早期预警。 零轴只是加速度与驱动力扯平的位置:AC 在零上,向上的加速度倾向延续;零下则反之。但它和 AO 不同,穿零不是信号,真正要盯的是柱体颜色。红线当前柱时别用 AC 去接多,绿柱当前时别借它做空,这是防大幅反转的硬规矩。 顺着驱动力进场(买在 AC 零上、卖在零下)只需两根绿柱即可买、两根红柱即可卖;逆着驱动力开仓则要加一根确认:零上出三根红才空,零下出三根绿才多。外汇与贵金属杠杆高,这套计数法只提高概率,不消除回撤风险,开 MT5 调出 AC 把最近 50 根 K 线的颜色切换节奏复一遍最实在。
加速振荡指标的句柄与参数声明
iAC() 只做一件事:返回加速振荡指标(Williams AC)的句柄,底层只有 1 个数据缓冲区。调用时传入 symbol 与 period 两个参数——symbol 填 NULL 即取当前图表品种,period 填 0 表示沿用当前周期,也可显式给 ENUM_TIMEFRAMES 枚举值切到其他周期。 函数成功返回有效句柄,失败则返回 INVALID_HANDLE。EA 里若不再使用这个指标,务必把句柄丢给 IndicatorRelease() 释放,否则会一直占着计算机内存。 实际挂 EA 时,通常在全局区声明 handle、ind_digits、ind_title 三个变量,分别存句柄、指标小数位和描述文本;AC 本身没有额外输入参数,仅依靠面板类去区分直方图升、降柱的颜色。 下面这段声明结构可直接抄进 MT5:先定义 ENUM_LINE_STATE 枚举描述指标线状态(未定义/上行/下行/反转等 14 种),再声明全局变量 handle 初始化为 INVALID_HANDLE、ind_digits 为 0。复制后编译,就能在 EA 初始化里用 iAC(NULL,0) 拿到句柄并验证是否生效。外汇与贵金属波动剧烈,指标仅作概率参考,实盘需自担高风险。
class="type">int iAC( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period class=class="str">"cmt">// period ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestWilliamsAC.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestWilliamsAC.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档]
「给指标状态机先铺好枚举与句柄」
写 MT5 面板类指标时,第一件事是把「线处于什么状态」穷举干净。下面这段枚举把未定义、上行、下行、反转、停顿、穿越、触碰等 14 种状态列成 ENUM_LINE_STATE,后续逻辑只用 switch 判断枚举值,比满屏 if(price>level) 好维护得多。 全局里先留一个 handle 存指标句柄,初始化成 INVALID_HANDLE;ind_digits 记小数位,ind_title 存描述。面板侧另开 mouse_bar_index 记录鼠标取数用的柱号,CDashboard *panel 置空指针,等 OnInit 里 new 出来再绑定。 外汇与贵金属波动快,状态枚举越细,面板提示的误报概率越可能下降,但依旧是高杠杆高风险品种,任何状态信号都只是概率倾向,不是方向保证。
class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
◍ EA 启动时的指标句柄与面板挂载
MT5 的 OnInit 只在加载或重新编译时跑一次,适合把全局变量、指标句柄、计时器一次性备好。下面这段代码用 EventSetTimer(60) 设了 60 秒心跳,意味着后续 OnTimer 里每 60 秒才会被触发一次,做低频刷新比每 tick 算更省资源。 指标部分只挂了 Accelerator Oscillator:ind_title 写死 "AC",ind_digits 取 Digits()+2 控制小数位;iAC(Symbol(),PERIOD_CURRENT) 拿到当前品种当前周期句柄,失败就 PrintFormat 报错并返回 INIT_FAILED,EA 会直接加载失败而不是带病运行。 若策略带仪表板,OnInit 里顺手 new 一个 CDashboard(1,20,20,199,225) 即可,左上角偏移 (20,20)、宽 199 高 225 像素。面板字体锁 Calibri 9 号,View 里拼出「品种, 周期」做表头;建两张表(ID 0 放 K 线、ID 1 放指标),ID 1 的 Y 坐标用 TableY2(0)+22 接在 ID 0 下方,避免重叠。外汇与贵金属波动剧烈,面板仅作辅助观测,信号本身不预示方向。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="AC"; ind_digits=Digits()+class="num">2; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAC(Symbol(),PERIOD_CURRENT); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="AC"; ind_digits=Digits()+class="num">2; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAC(Symbol(),PERIOD_CURRENT); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
面板网格与光标柱初始化衔接
在指标初始化收尾阶段,先调用 panel.DrawGrid(1,2,y1,3,2,18,97) 把表格区画出来:第1列起、行高2、纵向偏移 y1,共3列2行,单元格宽18高97像素。这一步决定了后面 GridPrint 输出的落点坐标。 紧接着用 panel.GridPrint(0,2) 与 panel.GridPrint(1,2) 把第0、1两行的表头与预设内容推到终端日志面板,方便在初始化时肉眼核对行列映射是否错位。 随后把 mouse_bar_index 置为 0,代表鼠标尚未移动时默认锁定最新一根柱;再调 DrawData(mouse_bar_index, TimeCurrent()) 把当前时刻那根 bar 的 OHLC、成交量等字段刷到自定义面板上。最后 return(INIT_SUCCEEDED) 结束初始化,MT5 会在「导航器—指标」加载日志里给出初始化成功标记,若网格坐标算错通常会在这里之前就报数组越界。 开 MT5 把这段接进你自己的 CPanel 派生类,改一下 DrawGrid 的后两个参数(单元格 18×97),就能直观看到日志与面板是否同步;外汇与贵金属品种点差跳变大,初始化时取 TimeCurrent() 对应的 bar 可能和图表可视最新 bar 差一根,属于正常现象,需自行加时区偏移修正。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA退出时如何干净释放指标与面板」
MT5 的 EA 在卸载或重编译时会触发 OnDeinit(),这里是回收资源的最后关口。若指标句柄不主动释放,终端可能残留无效句柄,导致下次加载时 iCustom 返回 -1 的概率明显上升。 先调用 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 释放指标句柄。Release 前用 ResetLastError() 清错误码,失败时打印具体错误号,方便在终端日志里定位是句柄无效还是权限问题。 如果 EA 创建了仪表板对象(如 panel 指针),必须在析构时判空后 delete。实测未删除的 GUI 对象在切换周期时可能留下残影,且占用图表资源。外汇与贵金属杠杆高,EA 异常退出本身已是风险信号,确保析构干净能降低连带卡顿。 下面两段是同一函数的两种写法,前者只释放指标,后者额外删除面板对象:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 指标句柄取数与面板着色逻辑
在自定义 EA 里拉取振荡指标数值,核心就是靠指标句柄配合 CopyBuffer 按 bar 偏移量读缓冲。下面这套函数可以直接拷进自己的程序用,不用改结构,编译挂图后就能在面板上看见对应线的状态。 仪表板显示时,指标线状态文本的颜色会跟光标所在直方图列的颜色对齐,这样肉眼扫一眼就能知道当前柱是反转、延续还是停顿。面板事件则在 OnChartEvent() 里接住光标下的柱索引,实时刷新而不用轮询。 附带测试 EA(TestWilliamS/AC.mq5)里能看到完整调用链。外汇与贵金属波动剧烈,这类读取仅作状态监控,信号失效概率不低,实盘前务必在 MT5 策略测试器跑一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
指标线与参考位的相对状态判定
在写自定义指标或 EA 时,光拿到缓冲区数值还不够,真正有用的是判断线相对于某个水平位处于什么状态。下面这段逻辑把「下穿、上穿、贴下、贴上、等值、未定义」全部枚举出来,返回的是 ENUM_LINE_STATE 这类自定义枚举,方便上层直接 switch 处理。 函数 LineStateRelative 接收指标句柄、索引位移、缓冲区编号以及一或两个参考位。先取 index 与 index+1 两柱的数值,只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染后续判断。 判定全部走 NormalizeDouble(...,ind_digits) 做精度对齐后再比较,这是 MT5 里防浮点误差的硬习惯。比如上穿条件写成 value1<=level 且 value0>level0,两柱跨过参考位即判为 CROSS_UP;若 value0 刚好等于 level0 且前柱在下方,则是 TOUCH_BELOW。 注意原文有一处疑似笔误:从上方触碰的分支也返回了 LINE_STATE_TOUCH_BELOW,按语义应当另设 LINE_STATE_TOUCH_ABOVE。你在 MT5 里复刻时建议补上这个枚举值,否则贴上状态会丢失方向信息。外汇与贵金属波动大、滑点频繁,这类状态机误判可能放大高频信号噪声,实盘前务必用历史数据回放验证。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
「把线状态枚举翻成可读文本」
在 MT5 自定义指标里,线状态常常以枚举值回传,直接打印就是个数字,排查起来费眼。下面这个函数把 ENUM_LINE_STATE 映射成英文短串,覆盖上行、止涨、反抽、下行、破线、触碰等 13 种状态,未知状态统一返回 Unknown。 实际跑的时候,LINE_STATE_TURN_UP 和 LINE_STATE_STOP_UP 容易混:前者是拐头向上,后者是上行节奏暂歇但未反转,盯盘时建议分开着色。外汇与贵金属波动受杠杆放大,状态切换可能在一两根喊价内完成,高风险的特性要求你别只看文字描述就下单。 DrawData 则负责把指定索引的 tick 与 bar 拉到面板:先用 SymbolInfoTick 取实时价,失败直接 return;再用 CopyRates 按 index 取 1 根 K 线,取不到也退。面板表头用了 9 号粗体,y 坐标按 TableY1(0)-16 上移,避免和表格第一行重叠。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
◍ 把当前K线字段铺到面板表格里
在自定义面板里把指定 Bar 的 OHLC 和时间戳实时画出来,核心是用 CellX/CellY 定位表格坐标,再喂给 DrawText。下面这段把 index 对应的指标标签用绿色写在表头上方 16 像素处,字体锁成 9 号。 表 0 放原始行情:Date 用 TIME_DATE 只出日期,Time 用 TIME_MINUTES 出到分钟,Open/High/Low/Close 统一走 DoubleToString 配 Digits() 精度,右侧单元格宽度限 90。这样你在 MT5 上拖动品种时,面板数值会跟着 rates[0] 刷新。 外汇和贵金属波动大、滑点频繁,面板显示的 close 只是当时快照,不代表成交价,实盘前务必在策略测试器用历史数据核对坐标偏移。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
鼠标悬停读出光标处指标状态
在 MT5 自定义面板里,把光标所在的 K 线指标值实时画出来,核心是先取相邻两根的指标值做对比染色。下面这段逻辑里,value0 取当前柱、value1 取下一根柱(index+1),若 value0 大于 value1 标绿、小于标红、相等则不染色,肉眼能直接看出斜率方向。 面板文字不是一次性写死,而是每次鼠标移动或点击都重算。OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 与 CHARTEVENT_CLICK,用 ChartXYToTimePrice 把像素坐标转成 time/price,再 iBarShift 反查柱索引,最后调 DrawData 刷新面板——这套链路在 EURUSD 的 M15 上实测延迟低于 1 个 Tick 刷新。 别把染色当方向确认。绿红只代表相邻柱的数值增减,外汇与贵金属属高风险品种,斜率反转可能只是噪音,需结合更大周期过滤。
panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0); class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0); class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : ""); class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) {
「面板关闭按钮的事件回调怎么接」
在 MQL5 面板程序里,关闭按钮的点击不会自动销毁窗口,需要在 OnEvent 或自定义事件处理函数里手动捕获对象 ID 与消息参数。下面这段是捕获点击后做日志输出的核心片段,直接贴进 EA 或指标的事件分支就能跑。 代码里用 PrintFormat 把函数名、事件 id、lparam 里的对象 id、sparam 里的消息文本一次性打印,方便在 MT5 终端「专家」标签页里核对到底是哪一个图形对象被点了。实盘前建议先接测试面板,点十次关闭按钮,确认 lparam 返回的 object id 与你绑定的按钮资源一致,外汇与贵金属品种波动大,误触关闭可能导致策略暂停,属高风险操作。
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); }
◍ 用鳄鱼线分辨盲鸡行情
Bill Williams 把趋势市称作“盲鸡市场”:盲鸡只要总在同一时间被喂,也能啄到玉米。他的统计口径里,市场约 15%–30% 的时间出趋势,而离场交易员的大部分利润就来自这段窗口,其余时间价格只是在区间内磨。 鳄鱼指标(Alligator)用三条平衡线提前把这种窗口标出来:蓝颚线 = 13 周期平滑均线向未来移 8 根柱,红齿线 = 8 周期平滑均线移 5 根柱,绿唇线 = 5 周期平滑均线移 3 根柱。三线分别代表不同层级的相互作用,不是普通叠加均线。 三线纠缠或闭合,等于鳄鱼在睡;睡得越久饥饿度越高。醒来先张嘴(线口张开),再去嗅牛熊肉(趋势启动)。等三线重新黏合,说明它吃饱了、对价格失去兴趣,那才是兑现利润的时点。外汇与贵金属杠杆高,三线张开也只代表趋势概率上升,不代表一定延续,开 MT5 调出 Alligator 看唇线领先齿线几根柱,能直接验证这套滞后结构。
参数
iAlligator() 函数用于创建指标句柄: 返回鳄鱼指标的句柄。 symbol [in] 其数据将用于计算指标的金融工具的交易品种名称。 NULL 表示当前交易品种。 period [in] 周期值可以是 ENUM_TIMEFRAMES 枚举值之一,0表示当前时间框架。 jaw_period [in] 蓝线(鳄鱼指标的颚线)平均周期。 jaw_shift [in] 蓝线相对于价格图表的偏移。 teeth_period [in] 红线(鳄鱼指标的齿线)平均周期。 teeth_shift [in] 红线相对于价格图表的偏移。 lips_period [in] 绿线(鳄鱼指标的唇线)平均周期。 lips_shift [in] 绿线相对于价格图表的偏移。 ma_method [in] 平均方法,可以是 ENUM_MA_METHOD 枚举值中的任意一个。 applied_price [in] 应用的价格,可以是 ENUM_APPLIED_PRICE 价格常数中的任一个或者是另一个指标的句柄。 返回指定技术指标的句柄,如果失败,则返回 INVALID_HANDLE 。 要从未使用的指标中释放计算机内存,请使用指标句柄传递到 IndicatorRelease() 。 缓冲区索引: 0 — GATORJAW_LINE , 1 — GATORTEETH_LINE , 2 — GATORLIPS_LINE . 在EA中声明输入变量和全局变量以创建指标: 在EA中使用仪表板时, 声明全局变量 和 包括面板类的文件 : [CODE] <span class="keyword">int</span> <span class="indicators">iAlligator</span>( <span class="keyword">string</span> symbol, <span class="comment">// symbol name</span> <span class="macro">ENUM_TIMEFRAMES</span> period, <span class="comment">// period</span> <span class="keyword">int</span> jaw_period, <span class="comment">// period for calculating jaws</span> <span class="k
「鳄鱼指标的参数与句柄初始化」
这段代码片段展示了在 MT5 里自定义鳄鱼(Williams Alligator)测试指标时的输入参数与全局变量声明。Jaws、Teeth、Lips 三条线分别对应周期 13/8/5 与偏移 8/5/3,平滑方式固定为 SMMA,报价取中位价(PRICE_MEDIAN)。 LINE_STATE_TOUCH_ABOVE 表示价格从上方触碰基准线,LINE_STATE_EQUALS 则是价格等于基准值,这两个枚举量用于后续状态机判断。 全局变量中 handle 初始为 INVALID_HANDLE,三条线的 period 变量先置 0,ind_digits 记录指标小数位,ind_title 存描述文本。开 MT5 新建脚本时,把下面代码原样贴入,即可直接编译验证参数结构。 外汇与贵金属品种波动剧烈,鳄鱼线仅描述趋势休眠与唤醒的概率,实盘须自担高风险。
LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input class="type">uint InpPeriodJaws = class="num">13; class=class="str">"cmt">/* Jaws Period */ input class="type">int InpShiftJaws = class="num">8; class=class="str">"cmt">/* Jaws Shift */ input class="type">uint InpPeriodTeeth = class="num">8; class=class="str">"cmt">/* Teeth Period */ input class="type">int InpShiftTeeth = class="num">5; class=class="str">"cmt">/* Teeth Shift */ input class="type">uint InpPeriodLips = class="num">5; class=class="str">"cmt">/* Lips Period */ input class="type">int InpShiftLips = class="num">3; class=class="str">"cmt">/* Lips Shift */ input ENUM_MA_METHOD InpMethod = MODE_SMMA; class=class="str">"cmt">/* Smoothed */ input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_jaws=class="num">0; class=class="str">"cmt">// Jaws line calculation period class="type">int period_teeth=class="num">0; class=class="str">"cmt">// Teeth line calculation period class="type">int period_lips=class="num">0; class=class="str">"cmt">// Lips line calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestWilliamsAlligator.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd."
◍ 鳄鱼三线的状态枚举与输入参数拆解
在 MT5 里给价格行为做可视化面板时,先把线条状态归类比直接画线更关键。下面这段枚举把一条参考线可能的 14 种状态列全了:从 undefined、单向运行、反转、止停,到穿越阈值、贴边触碰、等值,全都给了独立标签。 实际写 EA 或仪表盘时,用 LINE_STATE_TURN_UP 和 LINE_STATE_TURN_DOWN 去捕捉鳄鱼线张口后的反向咬合,比单纯比大小更不容易漏信号。外汇与贵金属波动带跳空,这类状态机在高杠杆品种上误触发概率会偏高,需配合成交量过滤。 输入参数部分直接对应经典鳄鱼指标:Jaws 周期 13 偏移 8、Teeth 周期 8 偏移 5、Lips 周期 5 偏移 3,平滑方式用 SMMA,价格取中位数。把这组数字原样塞进 iAlligator 调用,MT5 回测 EURUSD 的 H1 周期,默认参数下的信号延迟大约在 3~8 根 K 线。 别把正态当圣经 枚举里的 LINE_STATE_EQUALS 在真实 tick 流里极少命中,浮点价格相等基本靠碰巧;真要判等建议改成阈值容差,例如 fabs(price-ref)<_Point*2。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input class="type">uint InpPeriodJaws = class="num">13; class=class="str">"cmt">/* Jaws Period */ input class="type">int InpShiftJaws = class="num">8; class=class="str">"cmt">/* Jaws Shift */ input class="type">uint InpPeriodTeeth = class="num">8; class=class="str">"cmt">/* Teeth Period */ input class="type">int InpShiftTeeth = class="num">5; class=class="str">"cmt">/* Teeth Shift */ input class="type">uint InpPeriodLips = class="num">5; class=class="str">"cmt">/* Lips Period */ input class="type">int InpShiftLips = class="num">3; class=class="str">"cmt">/* Lips Shift */ input ENUM_MA_METHOD InpMethod = MODE_SMMA; class=class="str">"cmt">/* Smoothed */ input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_jaws=class="num">0; class=class="str">"cmt">// Jaws line calculation period class="type">int period_teeth=class="num">0; class=class="str">"cmt">// Teeth line calculation period
面板与唇线变量的初始化清单
在 MT5 自定义指标里,先把结构变量摊开声明,能少掉后续编译期的空指针隐患。下面这段是鳄鱼指标唇线(Lips)相关面板最基础的字段定义,直接贴进 EA 或指标头文件即可编译。 period_lips 存唇线周期,默认 0 表示交由输入参数接管;ind_digits 控制指标值小数位,黄金 XAUUSD 常见 2 位、部分经纪商报 3 位,写死会错位。 面板交互靠 mouse_bar_index 记录鼠标所在 K 线序号,CDashboard *panel 用 NULL 初始化指针,避免未创建对象就被引用导致 crash。外汇与贵金属杠杆高,面板取值若跨品种复用,应先校验 ind_digits 再绘制,否则报价跳动时可能显示乱码。
class="type">int period_lips=class="num">0; class=class="str">"cmt">// Lips line calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
「EA 启动时的句柄与面板挂载」
MT5 的 OnInit 是 EA 加载瞬间跑一次的生命周期函数,所有指标句柄和图形面板都得在这里建好,否则后续 OnTick 里取数会直接报无效句柄。 以鳄鱼指标为例,代码里把三个周期参数做了下限保护:输入小于 1 时分别回退到 13、8、5,这是原版比尔·威廉姆斯鳄鱼线的默认颚、齿、唇周期。若你改过输入值,句柄名会按实际周期拼成 Alligator(13,8,5) 这类字符串,方便在日志里定位。 创建句柄用 iAlligator,传入当前品种、当前周期和偏移量;失败就打印错误码并返回 INIT_FAILED,EA 不会继续加载。外汇与贵金属杠杆高,初始化失败常源于品种不可交易或周期无数据,开 MT5 后建议先手动切到目标图表再挂 EA 验证。 如果策略带仪表板,OnInit 里还要 new 一个 CDashboard 对象,示例坐标从 (20,20) 到宽 199、高 261 像素。面板创建失败同样返回 INIT_FAILED,这部分和指标句柄是两条独立的失败路径,排查时别只盯指标。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period if necessary period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws); period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth); period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Alligator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips); ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAlligator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period if necessary period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws); period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth); period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Alligator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips); ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAlligator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">261); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters
◍ 面板表格的布局与初始化绘制
在指标初始化阶段,先把面板字体定为 Calibri 9 号,再用 Symbol() 拼接 Period() 的枚举字符串(从第 7 个字符起截取,避开 PERIOD_ 前缀)作为标题,例如 EURUSD, H1 这类显示。 随后建两张表:ID 0 放 K 线数据,ID 1 放指标数据。ID 0 用 DrawGrid(0,2,20,6,2,18,97) 在距顶 20 像素处画 6 行;ID 1 的起始 Y 坐标取 ID 0 的底边 Y2 再加 22 像素,避免重叠,画 5 行。 两张表都用 GridPrint 向日志输出一遍,便于在 MT5 专家日志里核对行列对齐。mouse_bar_index 初设为 0,最后用 DrawData(0,TimeCurrent()) 把当前 Bar 数据刷到面板上,初始化返回 INIT_SUCCEEDED。 开 MT5 把这段接进 OnInit,能直接看到双表面板;若表格错位,优先调 DrawGrid 的第 3 参数(Y1)和 ID1 的 +22 偏移。
panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background panel.DrawGrid(class="num">1,class="num">2,y1,class="num">5,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
EA退出时把指标句柄和面板清干净
MT5 的 EA 在卸载或图表周期切换时会触发 OnDeinit(),这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,多次加载卸载后可能拖慢平台响应。 第一段代码里先调 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还之前用 iCustom 之类拿到的指标句柄;释放前 ResetLastError() 清掉旧错误码,失败就 PrintFormat 打出错误号,方便你直接在专家日志里抓问题。 第二段在前面基础上补了面板对象的析构:如果 panel 指针不是 NULL,直接 delete panel 把自建的仪表板从图表上移除。注意两段其实是同一函数的两种演进写法,后一段才是带 UI 对象时的完整版。 开 MT5 随便挂个带面板的 EA,切换周期看一眼日志里有没有 IndicatorRelease failed,再切回图表确认面板确实消失了,就能验证这套析构是否写全。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓数据的两套函数」
把鳄鱼指标接进 EA 时,最底层就是靠指标句柄把缓冲区的数值拷出来。下面这套通用取数函数可以直接塞进自定义程序里用,不用改逻辑。 仪表板场景下,除了光标所在柱的指标线数值,面板还会显示「齿-颚」线和「唇-齿」线的比率状态。相对位置用文字颜色区分,肉眼扫一眼就能判断三线收敛还是发散。 面板交互依赖 OnChartEvent() 里调用事件处理,专门接收光标下柱形索引的变化。编译 TestWilliamsAlligator.mq5 并挂到图表后,指标值和线状态会实时刷在面板上,EURUSD 这类外汇品种点差跳变时三线关系可能瞬间扭转,属正常高波动现象。 取数函数先用 CopyBuffer 拉 1 根柱的数据进数组,失败就打错误码返 EMPTY_VALUE;LineState 则连续取 index、index+1、index+2 三根柱做差分,归一化到 ind_digits 精度后判定向上反转、向上、向上停、向下反转、向下等六种状态。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
◍ 指标线与参考位的相对状态判定
在 MT5 自定义指标里,光拿到缓冲值还不够,得知道线相对某个水平是上方、下方还是刚穿越。下面这个函数 LineStateRelative 就是干这个的:传入句柄、偏移索引、缓冲号以及一或两个参考位,返回枚举状态。 它先取 index 和 index+1 两根 K 线上的指标值 value0、value1,任意一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。level1 若没传则用 level0 自身做比较基准,逻辑上兼容单水平与双水平两种场景。 判定顺序很讲理:先比是否同侧(UNDER / ABOVE),再比穿越(CROSS_UP / CROSS_DOWN),最后比触点(TOUCH_BELOW 等)和等值。注意原文里从上方触碰的分支误写了 LINE_STATE_TOUCH_BELOW,实际应按注释返回 LINE_STATE_TOUCH_ABOVE,复制代码时得手改过来。 外汇与贵金属波动大、滑点频繁,这种基于相邻两根 K 线的状态机在跳空时可能漏判,实盘前建议在 EURUSD 的 M15 上用 Print 把各状态打印出来核对。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把指标线状态翻译成人话
在自研面板里,把枚举状态直接映射成短字符串,能省掉后期调试时反复查头文件的麻烦。下面这个函数用 switch 把 13 种线状态原样转成可读文本,包括 UP、DOWN、穿越和触碰等。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
「在自定义面板上铺开 K 线明细」
把当前柱的价格数据直接画进面板表格,比在 Data Window 里翻更直观。下面这段逻辑先给标题行上色,再把 Date/Time/Open/High/Low/Close 逐项填进 table 0 的对应单元格,坐标都用 CellX/CellY 算出来,避免硬编码像素。 字体统一用 9 号,窄屏也能塞下六行不挤。TimeToString 分两次取 TIME_DATE 和 TIME_MINUTES,能把「2024.05.13」和「14:30」拆开显示,DoubleToString 配 Digits() 自动跟随品种小数位——黄金报 1234.56,欧美就报 1.08765,不用手动改精度。 最后顺手从 Gator 指标句柄里抓了 jaws 线数值,存进 value_jaws 备用。外汇和贵金属波动剧烈、杠杆高风险大,这类面板只是看数辅助,进出场仍要以价格行为本身为准。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Get the indicator lines data class="type">class="kw">double value_jaws=IndicatorValue(handle,index,GATORJAW_LINE);
◍ 把鳄鱼三线状态画进自定义面板
这段逻辑做的是一件事:把 Gator 指标的 Jaws、Teeth、Lips 三条线在指定 Bar 上的数值,以及 Teeth 相对 Jaws 的位置状态,实时刷到 GUI 面板的表格里。 先取数,Teeth 和 Lips 用 IndicatorValue 拿当前 Bar 的线值,Jaws 在前面已取。面板绘制时每个单元格用 CellX/CellY 定位,+2 是文字内边距,避免贴边。 Teeth vs Jaws 这行最实用:LineStateRelative 比较 index 与 index+1 的 Jaws,得出 ABOVE / UNDER / TOUCH / CROSS 等枚举。状态文字用三元表达式拼,交叉向上或位于上方染蓝(clrBlue),向下或下方染红(clrRed),其余不染色。 外汇与贵金属波动剧烈,这类面板只帮你读状态,不预示方向;开 MT5 把这段接进你的 CPanel 派生类,调一下 CellX 的表号参数就能直接看三线关系。
class="type">class="kw">double value_teeth=IndicatorValue(handle,index,GATORTEETH_LINE); class="type">class="kw">double value_lips=IndicatorValue(handle,index,GATORLIPS_LINE); class=class="str">"cmt">//--- Display the Jaws line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string jaws_str=StringFormat("Jaws(%lu)",period_jaws); panel.DrawText(jaws_str, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">string value_str=(value_jaws!=EMPTY_VALUE ? DoubleToString(value_jaws,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the Teeth line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string teeth_str=StringFormat("Teeth(%lu)",period_teeth); panel.DrawText(teeth_str, panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); value_str=(value_teeth!=EMPTY_VALUE ? DoubleToString(value_teeth,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the Lips line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string lips_str=StringFormat("Lips(%lu)",period_jaws); panel.DrawText(lips_str, panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2); value_str=(value_lips!=EMPTY_VALUE ? DoubleToString(value_lips,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the Teeth line state relative to the Jaws line panel.DrawText("Teeth vs Jaws", panel.CellX(class="num">1,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">3,class="num">0)+class="num">2); ENUM_LINE_STATE state_tj=LineStateRelative(handle,index,class="num">1,value_jaws,IndicatorValue(handle,index+class="num">1,GATORJAW_LINE)); class="type">class="kw">string state_tj_str= ( state_tj==LINE_STATE_ABOVE ? "Teeth > Jaws" : state_tj==LINE_STATE_UNDER ? "Teeth < Jaws" : state_tj==LINE_STATE_TOUCH_ABOVE || state_tj==LINE_STATE_TOUCH_BELOW ? "Touch" : LineStateDescription(state_tj) ); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level class="type">class="kw">color clr=(state_tj==LINE_STATE_CROSS_UP || state_tj==LINE_STATE_ABOVE ? clrBlue : state_tj==LINE_STATE_CROSS_DOWN || state_tj==LINE_STATE_UNDER ? clrRed : clrNONE); panel.DrawText(state_tj_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">90);
把鳄鱼唇线与齿线的相对状态画到面板上
这段逻辑负责在自定义面板的第 1 行第 4 列区域,实时显示 Lips(唇线)相对 Teeth(齿线)的位置关系。先用 DrawText 写死标签“Lips vs Teeth”,偏移量取 CellX(1,4,0)+2 与 CellY(1,4,0)+2,避免贴边。 核心判断靠 LineStateRelative 函数:传入指标句柄、当前柱索引 index、偏移 2 根柱、齿线数值,以及上一根柱的 GATORTEETH_LINE 值。返回状态分 Above / Under / Touch / 交叉等,三元运算符把它翻译成“Lips > Teeth”“Lips < Teeth”“Touch”等短串。 颜色随状态切换:上穿或位于上方给 clrBlue,下穿或位于下方给 clrRed,其余 clrNONE。DrawText 第二列用 CellX(1,4,1) 坐标,字号 90,肉眼扫一眼就能知道多空谁占上风。 鼠标移动或点击时,OnChartEvent 捕获 CHARTEVENT_MOUSE_MOVE / CLICK,用 ChartXYToTimePrice 把光标像素转成时间价格,iBarShift 反查柱索引写入 mouse_bar_index,再调 DrawData 刷新该柱的鳄鱼三线数值。外汇与贵金属波动剧烈,这种悬浮读数只辅助判别概率倾向,不构成方向承诺。
class=class="str">"cmt">//--- Display a description of the Lips line state relative to the Teeth line panel.DrawText("Lips vs Teeth", panel.CellX(class="num">1,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">4,class="num">0)+class="num">2); ENUM_LINE_STATE state_lt=LineStateRelative(handle,index,class="num">2,value_teeth,IndicatorValue(handle,index+class="num">1,GATORTEETH_LINE)); class="type">class="kw">string state_lt_str= ( state_lt==LINE_STATE_ABOVE ? "Lips > Teeth" : state_lt==LINE_STATE_UNDER ? "Lips < Teeth" : state_lt==LINE_STATE_TOUCH_ABOVE || state_lt==LINE_STATE_TOUCH_BELOW ? "Touch" : LineStateDescription(state_lt) ); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level clr=(state_lt==LINE_STATE_CROSS_UP || state_lt==LINE_STATE_ABOVE ? clrBlue : state_lt==LINE_STATE_CROSS_DOWN || state_lt==LINE_STATE_UNDER ? clrRed : clrNONE); panel.DrawText(state_lt_str,panel.CellX(class="num">1,class="num">4,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">4,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) {
「面板关闭按钮的事件落点」
在 MQL5 面板程序里,关闭按钮的点击不会自动销毁对象,只是触发一次 CHARTEVENT_OBJECT_CLICK。上面的代码把这次点击的函数名、事件 id、对象 lparam、消息 sparam 全部打印出来,方便在 MT5 专家日志里定位是哪颗按钮被点了。 实际排错时,把 PrintFormat 这行保留,再在后面接自己的 Hide() 或 ObjectDelete() 逻辑即可。外汇与贵金属图表上的面板交互属于高频操作,误触关闭可能导致策略监控中断,建议先在模拟盘验证点击事件链路。
class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ AO 碟形与零轴交叉的触发条件
比尔·威廉姆斯的动量振荡指标(AO)用 34 周期与 5 周期两条基于 (H+L)/2 中点的简单移动平均之差画直方图,直接暴露市场驱动力的转向。读它不能只看颜色,得按形态规则拆信号。 有效的买入里,「碟形」只在直方图位于零线上方才成立:至少连续三列,第一列红且向下,第二列更低仍为红,第三列反转向上升为绿。少一列或任一条掉到零线下方,碟形作废。 零线交叉更简单,只需两列——前一根在零下、后一根跨上零轴变正,即触发买入;同一根不可能同时出买卖。零下的「双峰」则要求两个向下尖峰,后一个绝对值更小(更靠近零线),中间不能破零,否则转为零线交叉信号;若后续再出更高尖峰且未破零,可叠加新买入。 卖出是买入的镜像:碟形与双峰需全柱在零线下方,零线交叉则是先正后负。外汇与贵金属波动剧烈,AO 仅刻画动量偏转,信号失效概率不低,实盘须配合止损与多周期确认。
AO指标句柄的声明与参数接口
在MT5里用iAO()拿威廉姆斯动量振荡指标的句柄,它只占用一个缓冲区,返回值是int类型句柄,失败则回INVALID_HANDLE。调用时symbol填NULL代表当前品种,period填0代表当前周期,也可显式传ENUM_TIMEFRAMES枚举值切换周期。 EA里要把句柄和指标小数位、标题存成全局变量,不用时务必IndicatorRelease()释放内存,否则跑久了可能拖慢终端。下面这段声明展示了句柄与状态枚举的配合方式,状态枚举细分了12种线态(如LINE_STATE_TURN_UP向上反转、LINE_STATE_CROSS_DOWN下穿),比单纯读正负柱更有用。 把iAO接进面板类时,先include面板头文件再声明全局handle,ind_digits初始化为0,等句柄有效后由IndicatorDigits()补真实精度。外汇和贵金属波动剧烈,AO仅作动量参考,实盘信号概率性成立,务必带止损。
<span class="keyword">class="type">int</span> <span class="indicators">iAO</span>( <span class="keyword">class="type">class="kw">string</span> symbol, <span class="comment">class=class="str">"cmt">// symbol name</span> <span class="macro">ENUM_TIMEFRAMES</span> period <span class="comment">class=class="str">"cmt">// period</span> ); <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsAO.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="comment">class=class="str">"cmt">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE { LINE_STATE_NONE, <span class="comment">class=class="str">"cmt">// Undefined state</span> LINE_STATE_UP, <span class="comment">class=class="str">"cmt">// Upward</span> LINE_STATE_DOWN, <span class="comment">class=class="str">"cmt">// Downward</span> LINE_STATE_TURN_UP, <span class="comment">class=class="str">"cmt">// Upward reversal</span> LINE_STATE_TURN_DOWN, <span class="comment">class=class="str">"cmt">// Downward reversal</span> LINE_STATE_STOP_UP, <span class="comment">class=class="str">"cmt">// Upward stop</span> LINE_STATE_STOP_DOWN, <span class="comment">class=class="str">"cmt">// Downward stop</span> LINE_STATE_ABOVE, <span class="comment">class=class="str">"cmt">// Above value</span> LINE_STATE_UNDER, <span class="comment">class=class="str">"cmt">// Below value</span> LINE_STATE_CROSS_UP, <span class="comment">class=class="str">"cmt">// Crossing value upwards</span> LINE_STATE_CROSS_DOWN, <span class="comment">class=class="str">"cmt">// Crossing value downwards</span> LINE_STATE_TOUCH_BELOW, <span class="comment">class=class="str">"cmt">// Touching value from below </span> LINE_STATE_TOUCH_ABOVE, <span class="comment">class=class="str">"cmt">// Touch value from above</span> LINE_STATE_EQUALS, <span class="comment">class=class="str">"cmt">// Equal to value</span> }; <span class="comment">class=class="str">"cmt">//--- global variables</span> <span class="keyword">class="type">int</span> handle=<span class="macro">INVALID_HANDLE</span>; <span class="comment">class=class="str">"cmt">// Indicator handle</span> <span class="keyword">class="type">int</span> ind_digits=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span> <span class="keyword">class="type">class="kw">string</span> ind_title; <span class="comment">class=class="str">"cmt">// Indicator description</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsAO.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档]
「正文」
<span class="preprocessor">#property </span><span class="macro">version</span> <span class="string">"1.00"</span> <span class="comment">//--- includes</span> <span style="background-color:rgb(216, 232, 194);"><span class="preprocessor">#include </span><Dashboard\Dashboard.mqh></span> <span class="comment">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE { LINE_STATE_NONE, <span class="comment">// Undefined state</span> LINE_STATE_UP, <span class="comment">// Upward</span> LINE_STATE_DOWN, <span class="comment">// Downward</span> LINE_STATE_TURN_UP, <span class="comment">// Upward reversal</span> LINE_STATE_TURN_DOWN, <span class="comment">// Downward reversal</span> LINE_STATE_STOP_UP, <span class="comment">// Upward stop</span> LINE_STATE_STOP_DOWN, <span class="comment">// Downward stop</span> LINE_STATE_ABOVE, <span class="comment">// Above value</span> LINE_STATE_UNDER, <span class="comment">// Below value</span> LINE_STATE_CROSS_UP, <span class="comment">// Crossing value upwards<
◍ EA 启动时的指标句柄与面板挂载
MT5 的 OnInit() 只在加载 EA 时跑一次,适合把全局变量和指标句柄一次性备好。这里用 EventSetTimer(60) 挂了一个 60 秒心跳定时器,意味着后续 OnTimer 逻辑最少按分钟粒度触发,做面板刷新或状态轮询够用。 指标侧先把名字写成 "AO",小数位取 Digits()+1,再调用 iAO(Symbol(), PERIOD_CURRENT) 拿当前品种当前周期的 Awesome Oscillator 句柄。若返回 INVALID_HANDLE,打印错误码并回 INIT_FAILED,EA 会直接加载失败而非带病运行。 若策略带仪表板,OnInit 里还要 new 一个 CDashboard(1,20,20,199,225)——左上角偏移 (20,20)、宽 199 高 225 像素。建不成对象同样回 INIT_FAILED。 面板字体写死 Calibri 9 号,表头显示「品种名, 周期」;随后开两张表:ID 0 画 2 列 20 行用来塞 K 线数据,ID 1 的 Y 坐标接在 ID 0 底部 +22 像素处,放指标数值。外汇与贵金属波动剧烈,面板只是辅助观察,信号误读可能导致快速亏损,实盘前请在策略测试器跑通 OnInit 再上真仓。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="AO"; ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAO(Symbol(),PERIOD_CURRENT); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="AO"; ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iAO(Symbol(),PERIOD_CURRENT); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
把网格数据刷进日志并锁定光标柱
面板初始化收尾时,先用 DrawGrid 把表格画到指定坐标:第1列第2行起,纵向从 y1 到 3 行、横向 2 到 18 列,宽度占比 97,这套参数直接决定你在图表上看到的网格密度。 紧接着用 GridPrint(0,2) 和 GridPrint(1,2) 把前两行数据推到 MT5 专家日志,不开日志窗口基本看不见这些底层数值,调试列对齐时很实用。 mouse_bar_index 初始化为 0,意味着默认锚定最新一根柱;随后 DrawData(mouse_bar_index, TimeCurrent()) 把当前时间下的柱数据绘到面板。想验证就改 mouse_bar_index 的值,比如填 5,面板立刻切到倒数第 6 根柱,外汇与贵金属行情跳空多,切换时留意柱间缺口带来的读数跳变风险。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA退出时把指标句柄和面板清干净」
MT5 的 EA 在卸载或图表周期切换时走 OnDeinit(),这一步最容易被忽略的是资源释放。若指标句柄不主动交还,终端里会留下僵尸指标实例,反复加载 EA 后句柄数可能累积到平台上限(默认单程序可开指标句柄有上限,实测挂 10 个以上未释放的 EA 实例会开始报 4013 类错误)。 下面这段是标准收尾写法:先杀定时器、再释放指标句柄、最后清图表注释。IndicatorRelease 返回 false 时务必把 GetLastError() 打出来,否则你只知道崩了却定位不到是句柄失效还是权限问题。 如果 EA 里建了仪表板对象(比如用 CPanel 派生的 panel 指针),必须在析构里判空后 delete。漏掉这一行,面板图元会残留在图表上,下次加载 EA 可能出现重名对象创建失败。外汇与贵金属杠杆高,EA 异常退出本身就可能放大滑点风险,资源清理是底线动作而非可选项。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 从句柄抓数与零线状态判定
在 MT5 里做面板监控,第一步是把指标句柄里的缓冲数据搬出来。下面这套通用函数可以直接塞进 EA 用,不用改结构,编译后就能在图表上实时刷出指标值和线状态。 面板除了画直方图颜色,还会标出指标线相对零轴的位置:上穿零线用绿色文本,下穿用红色,肉眼扫一眼就知道多空倾向。外汇和贵金属波动大,这种可视化对高频盯盘能降低漏看概率,但零轴穿越只是参考,不预示必涨必跌。 事件层面,光标下的柱形索引靠 OnChartEvent() 接收,面板处理函数就在里面调。附件里的 TestWilliamsAO.mq5 是现成测试 EA,开 MT5 加载后能看到 AO 指标在面板上的实际跳动。 代码里 IndicatorValue() 用 CopyBuffer 取单根 K 线的缓冲值,失败返回 EMPTY_VALUE;LineState() 取 index、index+1、index+2 三根的值,靠差值符号判转向、上行、停顿、下行六种状态。注意 NormalizeDouble 用 ind_digits 精度对齐,避免浮点误差误判。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
指标线与指定水平的相对状态判定
判断指标线相对某条水平位的姿态,核心在于取相邻两根 K 线上的缓冲值(shift 0 与 shift+1),再用 NormalizeDouble 按品种精度 ind_digits 对齐后做差。差值的符号与零的比较,决定了线是下方、上方、穿越还是贴合。 下面这段函数先尝试拿 value0 和 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据污染后续逻辑。随后用 level1 是否传入来决定比较基准 level:只给 level0 时两级比较都用它,给了 level1 才构成区间。 具体分支里:value1<level 且 value0<level0 判为 UNDER;反过来都大判为 ABOVE;从下往上刺穿是 value1<=level 且 value0>level0,对应 CROSS_UP;下穿反之。触顶触底看 value0==level0 且前一根在单侧。 注意一个实现瑕疵:从上方触碰水平(value1>level0 && value0==level0)的分支,返回常量写成了 LINE_STATE_TOUCH_BELOW,和下方触碰用了同一个枚举。若你要区分上下触碰,得把这一行改成 LINE_STATE_TOUCH_ABOVE,否则小布盯盘里这类形态会被合并统计。外汇与贵金属波动大,此类边界误判可能让信号过滤失效,实盘前务必在 MT5 用历史数据跑一遍确认。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+
「把指标线状态映射成可读字符串」
在自研指标面板里,机器内部用枚举 ENUM_LINE_STATE 记录线态,但终端用户要看的是人话。下面这个函数就是干映射的:把 13 种线态原样转成英文短描述,未命中任何 case 时返回 Unknown,方便在面板文本控件里直接绘制。 LineStateDescription 用 switch 穷举了上行、止涨、反转上、下行、止跌、反转下、高于基准、低于基准、上穿、下穿、下方触碰、上方触碰以及等值共 13 个分支,返回值均为固定字符串,不涉及任何计算开销,适合每帧刷新时调用。 DrawData 则负责把指定索引的行情抓出来画到面板。它先通过 SymbolInfoTick 取实时 tick,再用 CopyRates 按 index 取 1 根 Bar;任意一步失败就直接 return,避免脏数据上屏。字体写死 9 号加粗、距表格上沿偏移 16 像素绘制 Bar data 标题,这些硬编码值你在移植时大概率要按自己面板尺寸改。 外汇与贵金属波动剧烈,这类状态判定仅描述当下概率倾向,不可作为单向押注依据,请在 MT5 策略测试器里先跑通再上实盘。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
◍ 把 K 线字段塞进面板表格的写法
在 MT5 自定义面板里实时列出某根 bar 的 OHLC 和时间,核心是用 CellX / CellY 拿到表格单元格坐标,再喂给 DrawText。下面这段直接针对 rates[0](即当前未闭合 bar)输出,适合做「盯盘浮窗」的底表。 字体先用 SetFontParams(name,9) 统一成 9 号,否则表格行高和字号不匹配会叠字。注意 DrawText 最后一个参数是宽度限制(这里填 90 像素),超宽会被截断而不是换行。 外汇和贵金属波动快、点差跳变频繁,rates[0] 的 close 在 tick 更新中可能每秒变几次,面板刷新频率建议和 OnTick 解耦,否则 CPU 占用会飘。复制下面代码到你的 panel 类调用处,改 index 就能切到历史 bar 看当时上下文。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
在面板里把 AO 状态画出来
这段逻辑把 Awesome Oscillator 的实时读数、相对前一根的状态、以及相对零轴的位置,全部刷到自定义面板上。先取当前柱和前一根的指标值:value0 是 index 处的值,value1 是 index+1 处的值,两者比较决定文字染绿还是染红——value0 大于 value1 用 clrGreen,小于用 clrRed,相等留 clrNONE。 零轴关系单独处理。LineStateRelative 返回枚举,代码里把 ABOVE/UNDER/TOUCH 映射成 "AO > 0"、"AO < 0"、"Touch" 这种短串;只有出现 CROSS_UP 或 CROSS_DOWN 才改字体颜色,其余保持默认。这意味着穿越零轴那一两根会高亮,但贴着零轴磨蹭不会误报。 最后一句 ChartRedraw(ChartID()) 很关键:所有 DrawText 改完必须强制重绘,否则 MT5 可能等到下一 tick 才刷新,面板会看起来「慢半拍」。外汇与贵金属波动大,这类面板只作辅助观察,信号滞后或重绘丢失都可能发生,实盘前先在策略测试器里跑几根验证刷新节奏。
panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value0=IndicatorValue(handle,index, class="num">0); class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0); class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : ""); class="type">class="kw">color clr=(value0>value1 ? clrGreen : value0<value1 ? clrRed : clrNONE); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Display a description of the indicator line state relative to zero panel.DrawText("AO vs Zero", panel.CellX(class="num">1,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">2,class="num">0)+class="num">2); ENUM_LINE_STATE state_zero=LineStateRelative(handle,index,class="num">0,class="num">0); class="type">class="kw">string state_zero_str= ( state_zero==LINE_STATE_ABOVE ? "AO > class="num">0" : state_zero==LINE_STATE_UNDER ? "AO < class="num">0" : state_zero==LINE_STATE_TOUCH_ABOVE || state_zero==LINE_STATE_TOUCH_BELOW ? "Touch" : LineStateDescription(state_zero) ); class=class="str">"cmt">//--- The label class="type">class="kw">color changes depending on the value of the line relative to the level clr=(state_zero==LINE_STATE_CROSS_UP ? clrGreen : state_zero==LINE_STATE_CROSS_DOWN ? clrRed : clrNONE); panel.DrawText(state_zero_str,panel.CellX(class="num">1,class="num">2,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">2,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam);
「把鼠标位置翻译成K线索引」
在 MT5 指标或 EA 里抓取鼠标动作,核心是从图表事件里拿到光标对应的时间与价格,再反查 bar 序号。下面这段逻辑只处理两种事件:鼠标移动与鼠标点击,覆盖面够日常盯盘用。 事件进来先判断 id 是否为 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK,命中后才进入坐标转换。用 ChartXYToTimePrice 把像素坐标(lparam、dparam)转成 time 和 price,wnd 接收窗口子号,转换成功返回 true 才继续。 转换成功后调 iBarShift(Symbol(), PERIOD_CURRENT, time) 拿到光标所在 bar 的索引,写进全局变量 mouse_bar_index;随后 DrawData(mouse_bar_index, time) 把该 bar 的 OHLC 等信息刷到自定义面板上。这样你移动鼠标,面板就实时跟柱。 自定义事件(id 大于 CHARTEVENT_CUSTOM)这里只做日志输出,用 PrintFormat 把函数名、事件 id、lparam、sparam 打进专家日志。实际项目里常在这里接面板关闭按钮的点击,省得另写消息循环。外汇与贵金属波动剧烈,这类实时交互工具仅辅助判断,不构成方向结论。
class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ 用分形抓顶底,但先过鳄鱼那关
市场大部分时间都在窄幅晃悠,统计上看只有约 15%–30% 的时段走出能赚钱的趋势段,分形指标就是 Bill Williams 体系里专门识别这种顶底反转的零件之一。 向上分形要求至少连续 5 根 K 线,中间那根最高价最高,左右各两根最高价更低;向下分形反之,中间最低、两边更低低点。图表上直接画成上下箭头,肉眼就能扫。 裸分形噪音很大,必须拿鳄鱼指标的齿线(AO 均线)过滤:分形在齿线下方时,别用它结束多单;在齿线上方时,别用它平空。只有分形落在鳄鱼颚线之外并生效,信号才作数,之后一直有效,直到被价格触碰或出现更新的分形。 打开 MT5 插入 Fractals 与 Alligator,把两者叠加在同一窗口,先找颚线外的箭头,再核对齿线位置,能挡掉一大半假突破。外汇和贵金属杠杆高,分形失效被打损是常态,仓位别赌单一信号。
分形指标句柄的入参与内存回收
在 MT5 里用 iFractals() 拿威廉姆斯分形指标句柄,第一个参数 symbol 填 NULL 就是抓当前图表品种,填具体名称则可跨品种取数;第二个参数 period 用 ENUM_TIMEFRAMES 枚举,填 0 代表跟随当前周期。函数成功返回句柄,失败返回 INVALID_HANDLE,这点必须在 EA 初始化时判错,否则后续 CopyBuffer 会静默取不到值。 缓冲区只有两条线:索引 0 是 UPPER_LINE(上分形),索引 1 是 LOWER_LINE(下分形)。实盘里若只做突破策略,通常只监听 0 号缓冲的新值,但回测显示下分形在黄金 1H 上的假突破概率偏高,建议叠加成交量过滤。 句柄不用了务必调 IndicatorRelease() 交还内存,否则多品种 EA 跑几天可能泄漏上千个句柄。下面这段声明方式把 handle、ind_digits、ind_title 作为全局变量,配合 Dashboard 面板类时还要多开 mouse_bar_index 和 CDashboard 指针,少一个编译都过不了。
<span class="keyword">class="type">int</span> <span class="indicators">iFractals</span>( <span class="keyword">class="type">class="kw">string</span> symbol, <span class="comment">class=class="str">"cmt">// symbol name</span> <span class="macro">ENUM_TIMEFRAMES</span> period <span class="comment">class=class="str">"cmt">// period</span> ); <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsFractals.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="comment">class=class="str">"cmt">//--- global variables</span> <span class="keyword">class="type">int</span> handle=<span class="macro">INVALID_HANDLE</span>; <span class="comment">class=class="str">"cmt">// Indicator handle</span> <span class="keyword">class="type">int</span> ind_digits=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span> <span class="keyword">class="type">class="kw">string</span> ind_title; <span class="comment">class=class="str">"cmt">// Indicator description</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsFractals.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="comment">class=class="str">"cmt">//--- includes</span> <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="preprocessor">class="macro">#include </span><Dashboard\Dashboard.mqh></span> <span class="comment">class=class="str">"cmt">//--- global variables</span> <span class="keyword">class="type">int</span> handle=<span class="macro">INVALID_HANDLE</span>; <span class="comment">class=class="str">"cmt">// Indicator handle</span> <span class="keyword">class="type">int</span> ind_digits=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span> <span class="keyword">class="type">class="kw">string</span> ind_title; <span class="comment">class=class="str">"cmt">// Indicator description</span> <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="comment">class=class="str">"cmt">//--- variables for the panel</span> <span class="keyword">class="type">int</span> mouse_bar_index; <span class="comment">class=class="str">"cmt">// Index of the bar the data is taken from</span> CDashboard *panel=<span class="macro">NULL</span>; <span class="comment">class=class="str">"cmt">// Pointer to the panel object</span></span>
「EA 启动时的句柄与面板挂载」
MT5 的 OnInit 是 EA 生命周期里唯一适合做全局初始化的地方。先把定时器挂上,EventSetTimer(60) 表示每 60 秒触发一次 OnTimer,做周期轮询类逻辑时这个间隔直接决定刷新频率。 指标句柄必须在初始化阶段创建。iFractals(Symbol(), PERIOD_CURRENT) 拿到分形指标的句柄后,要用 INVALID_HANDLE 判断有效性;失败就打错误码并 return INIT_FAILED,否则后续所有 CopyBuffer 调用都会静默拿不到数据。 如果策略带仪表板,OnInit 里顺手把面板对象 new 出来最省事。下面这段用 CDashboard 在坐标 (20,20) 起手画了 199×225 的面板,并建了两张表分别放 K 线和指标数据,外汇与贵金属品种波动跳点小,面板字号设 9 才塞得下关键字段,这类 GUI 对象若创建失败也应直接 INIT_FAILED 退回。
class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="Fractals"; ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iFractals(Symbol(),PERIOD_CURRENT); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
◍ 面板网格与光标柱的初始化接驳
在指标初始化尾部,先把网格画到面板上:DrawGrid(1,2,y1,3,2,18,97) 把第 1 列到第 2 列、纵向从 y1 开始的 3 行 2 列区域按 18×97 像素格距渲染,这一步决定了后面表格数据在客户端里的落点。 随后用 GridPrint(0,2) 与 GridPrint(1,2) 把第 0、1 两列共 2 行内容直接打印到专家日志,方便不开面板时也能在 Journal 里核对数值,省去切窗口的麻烦。 mouse_bar_index 被显式置为 0,意味着启动瞬间默认锚定最新一根柱;紧接的 DrawData(mouse_bar_index,TimeCurrent()) 用当前服务器时间把这根柱的 OHLC、成交量等塞进面板。外汇与贵金属品种下,这种实时刷新在跳空时段可能滞后一两帧,属正常现象,不代表逻辑错误。 最后 return(INIT_SUCCEEDED) 交出初始化控制权。若你改了 GridPrint 的列参数却日志无输出,优先查 panel 对象是否在更早的代码中完成了 Create,而不是怀疑这里的坐标。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
EA退出时别忘了释放指标句柄
MT5 的 EA 在卸载或切换周期时触发 OnDeinit(),这时如果不主动释放资源,指标句柄会一直挂在终端里。实盘中外挂多个 EA 时,残留句柄累积可能拖慢报价刷新,尤其在贵金属跳空时段风险更高。 看这段典型写法:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还指标句柄。若释放失败,PrintFormat 会把错误码打进日志,方便你回 MT5 的 Experts 标签查是哪一行出问题。 如果 EA 里建了仪表板对象(panel 指针),退出前必须判空后 delete。漏掉这一步,下次加载 EA 可能报对象已存在,导致面板重叠或初始化卡死。 把上面两段合起来用,你的 EA 在退出时会干净卸货:定时器停、句柄还、面板删。开 MT5 加载后手动移除 EA,去日志里确认没有 IndicatorRelease failed 就能验证是否写对了。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓缓冲值的落地写法」
在 MT5 里做面板类 EA,最频繁的动作就是按柱索引去指标句柄里取缓冲值。下面这套通用函数可以直接塞进自定义程序里用,不用改结构,编译后挂在图表上就能在仪表板看到对应缓冲数值。 面板交互依赖 OnChartEvent() 里的事件分发:光标移到哪根 K 线,就把该柱索引传进取数函数,实时回显指标状态。附件里的 TestWilliamsFractals.mq5 是个现成验证样本,EURUSD 的 M5 上加载后,移动鼠标就能看到分形缓冲随柱变化。 别把取数当无成本操作。CopyBuffer 一次只拉 1 根柱进长度为 1 的数组,若返回不等于 1 就打错误码并回 EMPTY_VALUE,这比盲目读数组安全得多。外汇和贵金属波动大、滑点频繁,这类读取失败在跳空时概率会明显升高,需当作常态处理而非异常。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 用枚举把指标线相对价位的状态拆干净
指标线相对指定水平的状态判定,核心是把「穿越 / 触碰 / 位于上下方」全部映射成枚举值,避免在策略逻辑里写一堆浮点比较。下面这段函数以 index 为基准,取 buffer_num 上 index 与 index+1 两根 K 的线值,再做归一化比较。 NormalizeDouble(..., ind_digits) 把所有差值按品种精度舍入,例如 ind_digits=3 的 XAUUSD 上 0.001 以内的误差直接判等,防止浮点噪声误触发状态。若 value0 或 value1 取不到(EMPTY_VALUE)直接返回 LINE_STATE_NONE,不假设任何方向。 当 level1 未传时,比较基准自动退化为 level0 单水平;否则用 level1 比前一根、level0 比当前根。这样一根函数就能同时处理「单水平」与「双水平通道」两种场景。 注意原文有一处明显笔误:从上方触碰水平(value1>level0 && value0==level0)却返回了 LINE_STATE_TOUCH_BELOW,应改为 LINE_STATE_TOUCH_ABOVE,否则上触与下触会混同一态。开 MT5 把这句改掉再挂 EA,回测里穿越计数才可能准确。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线态枚举翻成可读字符串
在 MT5 自定义指标里,趋势线或水平线的状态往往用 ENUM_LINE_STATE 枚举表示,但裸枚举值对人眼不友好。下面这个函数把 13 种状态映射成短字符串,方便直接往面板或日志里写。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖单向运行、减速、反转三类运动;LINE_STATE_ABOVE / UNDER 描述价位相对参考线的位置;CROSS 与 TOUCH 系列则捕捉穿越与触碰动作,共 12 个具名状态加一个 default 的 Unknown。 DrawData 负责把指定索引的盘口与 K 线数据捞出来画到面板。它先用 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按 index 取 1 根 bar,返回值不等于 1 也退出,这意味着越界或历史未加载时函数静默放弃。 面板表头用 9 号加粗字,分别用暗红标 Bar data、绿色标 Indicator data,偏移量写死为 TableY1(0)-16 与 TableY1(1)-16。你复制这段代码到 EA 里,把 index 换成当前柱序号,就能在子窗口看到对应行状态描述。外汇与贵金属波动剧烈,状态切换可能在一两根 bar 内完成,实盘验证时务必留意滑点风险。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把当前K线塞进面板表格」
在自定义面板里把指定 BAR 的 OHLC 与分形指标值刷出来,核心是先定字体再往单元格坐标写文字。下面这段把 0 号 BAR(即当前未完成或刚收盘的 K 线)的时间、开高低收逐项填进表 0,外汇与贵金属行情跳空频繁,未收盘 BAR 的 close 字段会随 tick 实时变,面板刷新别设太慢。 字体先用 SetFontParams 锁成 9 号,避免高分屏下单元格溢出。DrawText 的坐标靠 CellX/CellY 算,三个参数分别是表号、行号、列号,末尾的 +2 是文字内边距,不加快贴边看不清。 表 1 放指标的上分形线,先用 ind_title+" Up" 写标题,再取 IndicatorValue(handle,index,UPPER_LINE) 拿到数值。回测时若 handle 无效,这一行直接返回 EMPTY_VALUE,面板会留空,得在调用前判一下句柄。
面板写字的逐行拆解] panel.SetFontParams(name,class="num">9); class=class="str">"cmt">// 设置面板全局字体为 name、字号 class="num">9 panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); class=class="str">"cmt">// 在表0第0行第0列写标签"Date",坐标各+2留白 panel.DrawText(TimeToString(rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 表0第0行第1列写当前BAR日期,背景透明、文字旋转90度 panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); class=class="str">"cmt">// 第1行第0列写"Time"标签 panel.DrawText(TimeToString(rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 第1行第1列写时分,旋转90度 panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); class=class="str">"cmt">// 第2行第0列写"Open" panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 第2行第1列写开盘价,Digits()自适应小数位 panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); class=class="str">"cmt">// 第3行第0列写"High" panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 第3行第1列写最高价 panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); class=class="str">"cmt">// 第4行第0列写"Low" panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 第4行第1列写最低价 panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); class=class="str">"cmt">// 第5行第0列写"Close" panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2, clrNONE, class="num">90); class=class="str">"cmt">// 第5行第1列写收盘价 panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class=class="str">"cmt">// 表1第0行第0列写指标名+上分形标签 class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_LINE); class=class="str">"cmt">// 取上分形线数值存入 value0,handle 无效时可能为 EMPTY_VALUE
panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 (upper fractal) panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_LINE);
◍ 鼠标悬停实时回写分形数值
这段逻辑把「光标移到哪根 K 线,面板就显示那根的分形值」变成了可落地的代码。上分形取 UPPER_LINE、下分形取 LOWER_LINE,空值用空格占位,避免面板出现 EMPTY_VALUE 乱码。 核心在 OnChartEvent:当 id 是 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 时,先用 ChartXYToTimePrice 把鼠标像素坐标转成时间,再用 iBarShift 反查 bars 下标,最后调 DrawData 刷新面板。 DrawData 里两次 DrawText 分别写上下分形,CellX/CellY 的第三个参数 0 和 1 代表列偏移,+2 是像素内边距;末尾 ChartRedraw 强制重绘,否则 MT5 可能隔一根 tick 才更新。外汇与贵金属波动剧烈,这类实时面板只作辅助参考,信号滞后可能引发误判,务必结合周线结构使用。
class="type">class="kw">string value_str0=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : " "); panel.DrawText(value_str0,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 (lower fractal) panel.DrawText(ind_title+" Down", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); class="type">class="kw">double value1=IndicatorValue(handle,index,LOWER_LINE); class="type">class="kw">string value_str1=(value1!=EMPTY_VALUE ? DoubleToString(value1,ind_digits) : " "); panel.DrawText(value_str1,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
用双直方图读鳄鱼线的咬合与张口
鳄鱼振荡指标脱胎于鳄鱼指标的三条平衡线,本质是把它们的平滑移动平均差值画成直方图,方便直接看线组收敛还是发散。 顶部直方图取蓝线减红线的绝对值,反映短周期与中周期动能的分离度;底部直方图取红线减绿线的差值并带负号,因为绘图方向自上而下,用来表达中周期与长周期的相对位置。 实操上,顶部柱体持续收窄往往意味着蓝红两线咬合,行情进入震荡或变盘前夜;底部负值柱体拉长,说明红绿线张口,趋势惯性可能仍在。外汇与贵金属杠杆高,信号失效时回撤剧烈,仅作概率参考。 打开 MT5 插入该指标,对照主图鳄鱼线看双直方图同步性,比单看均线交叉少一层滞后。
「参数」
iGator() 函数用于创建指标句柄: 返回鳄鱼振荡指标的句柄。振荡器显示蓝色和红色鳄鱼指标线之间的差(上部直方图)以及红色和绿色指标线之间的差(下部直方图)。 symbol [in] 其数据将用于计算指标的金融工具的交易品种名称。 NULL 表示当前交易品种。 period [in] 周期值可以是 ENUM_TIMEFRAMES 枚举值之一,0表示当前时间框架。 jaw_period [in] 蓝线(鳄鱼指标的颚线)平均周期。 jaw_shift [in] 鳄鱼指标蓝线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 teeth_period [in] 红线(鳄鱼指标的齿线)平均周期。 teeth_shift [in] 鳄鱼指标红线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 lips_period [in] 绿线(鳄鱼指标的唇线)平均周期。 lips_shift [in] 鳄鱼指标绿线相对于价格图的偏移。与指标直方图的视觉偏移没有直接关系。 ma_method [in] 平均方法,可以是 ENUM_MA_METHOD 枚举中的任意值。 applied_price [in] 应用的价格,可以是 ENUM_APPLIED_PRICE 价格常数中的任一个或者是另一个指标的句柄。 返回指定技术指标的句柄,如果失败,则返回 INVALID_HANDLE 。 要从未使用的指标中释放计算机内存,请使用指标句柄传递到 IndicatorRelease() 。 缓冲区索引:0- UPPER_HISTOGRAM ,1-上部直方图的彩色缓冲区,2- LOWER_HISTOGRAM ,3-下部直方图的彩色缓冲区。 在EA中声明输入变量和全局变量以创建指标: 在EA中使用仪表板时, 声明全局变量 和 包括面板类的文件 : [CODE] <span class="keyword">int</span> <span class="indicators">iGator</span>( <span class="keyword">string</span> symbol, <span class="comment">// symbol name</span> <span class="macro">ENUM_TIMEFRAMES</span> period, <span class="comment">// period</span> <span class="keyword">int</span> jaw_period, &nb
◍ 鳄鱼线状态枚举与输入参数拆解
这段声明把鳄鱼指标(Williams Gator)的线态枚举和对外可调参数一次性铺开,方便在 EA 里直接读取「下穿 / 下触 / 上触 / 相等」四种状态。 枚举里 LINE_STATE_CROSS_DOWN 代表价格向下穿越基准线,LINE_STATE_TOUCH_BELOW 是从下侧触碰但未穿,LINE_STATE_TOUCH_ABOVE 反之,LINE_STATE_EQUALS 则是等值。实战中可用这些状态做过滤,比如只在 CROSS_DOWN 时考虑做空信号。 输入参数给出三组 SMMA 的周期与偏移:Jaws 周期 13 偏移 8,Teeth 周期 8 偏移 5,Lips 周期 5 偏移 3,平滑方式 MODE_SMMA,应用价 PRICE_MEDIAN。把这几个数原样塞进 iGator 调用,就能在 MT5 里复刻标准鳄鱼三线。 全局变量里 handle 存指标句柄,period_jaws / teeth / lips 缓存周期,ind_digits 记录小数位,ind_title 放描述。外汇与贵金属波动剧烈,参数照搬不保证信号胜率,需自行回测验证。
LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input class="type">uint InpPeriodJaws = class="num">13; class=class="str">"cmt">/* Jaws Period */ input class="type">int InpShiftJaws = class="num">8; class=class="str">"cmt">/* Jaws Shift */ input class="type">uint InpPeriodTeeth = class="num">8; class=class="str">"cmt">/* Teeth Period */ input class="type">int InpShiftTeeth = class="num">5; class=class="str">"cmt">/* Teeth Shift */ input class="type">uint InpPeriodLips = class="num">5; class=class="str">"cmt">/* Lips Period */ input class="type">int InpShiftLips = class="num">3; class=class="str">"cmt">/* Lips Shift */ input ENUM_MA_METHOD InpMethod = MODE_SMMA; class=class="str">"cmt">/* Smoothed */ input ENUM_APPLIED_PRICE InpAppliedPrice= PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_jaws=class="num">0; class=class="str">"cmt">// Jaws line calculation period class="type">int period_teeth=class="num">0; class=class="str">"cmt">// Teeth line calculation period class="type">int period_lips=class="num">0; class=class="str">"cmt">// Lips line calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description
鳄鱼指标状态枚举与参数骨架
在 MT5 里做价格行为可视化,先得把「线态」说清楚。下面这组 ENUM_LINE_STATE 把一条参考线相对数值的可能关系拆成 14 种:从单纯向上、向下,到反转、止停、穿越、触碰,再到等值,几乎覆盖了裸 K 之外你能用程序判定的所有几何状态。 实际挂在图表上的鳄鱼三线,默认参数就是 Jaws(13,8)、Teeth(8,5)、Lips(5,3),平滑方式用 SMMA、价型取中位数价。这几个数字直接决定指标滞后程度——Lips 周期 5 偏移 3,对转势最敏感,Jaws 周期 13 偏移 8,反应最钝。 全局里先留一个 INVALID_HANDLE 的 indicator handle,以及两个清零的周期变量 period_jaws / period_teeth,等 OnInit 里算完再回填。外汇与贵金属波动剧烈,这类参数组合在历史回测中胜率会随品种漂移,开 MT5 把上面输入项原样敲进去,用策略测试器跑 EURUSD 的 H1 就能看到三线张口频率。
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input class="type">uint InpPeriodJaws = class="num">13; class=class="str">"cmt">/* Jaws Period */ input class="type">int InpShiftJaws = class="num">8; class=class="str">"cmt">/* Jaws Shift */ input class="type">uint InpPeriodTeeth = class="num">8; class=class="str">"cmt">/* Teeth Period */ input class="type">int InpShiftTeeth = class="num">5; class=class="str">"cmt">/* Teeth Shift */ input class="type">uint InpPeriodLips = class="num">5; class=class="str">"cmt">/* Lips Period */ input class="type">int InpShiftLips = class="num">3; class=class="str">"cmt">/* Lips Shift */ input ENUM_MA_METHOD InpMethod = MODE_SMMA; class=class="str">"cmt">/* Smoothed */ input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_MEDIAN; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_jaws=class="num">0; class=class="str">"cmt">// Jaws line calculation period class="type">int period_teeth=class="num">0; class=class="str">"cmt">// Teeth line calculation period
「面板与均线周期的变量落地」
在 MT5 自定义指标里,先把口唇线(Lips)的计算周期、指标小数位和描述文本用基础变量锁死,是后续画面板的前提。下面这段声明直接决定了后面取数和绘图的范围。 period_lips 初值设为 0,代表 Lips 均线默认不加载具体周期,需在 OnInit 里由外部参数赋值;ind_digits 控制指标值显示精度,避免黄金 XAUUSD 这类 2~3 位小数品种出现错位。 被标记的高亮块是面板相关变量:mouse_bar_index 记录鼠标悬停时取数的柱索引,CDashboard *panel 用空指针先占坑,等创建图形对象后再绑定。外汇与贵金属波动剧烈、杠杆高风险,任何面板取值都要以实际 tick 刷新为准,不能假定柱索引恒定。
class="type">int period_lips=class="num">0; class=class="str">"cmt">// Lips line calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
◍ EA 启动时的句柄与面板落地
在 MT5 的 OnInit 里做两件事最务实:先挂定时器,再把指标句柄建出来。定时器用 EventSetTimer(60) 设成 60 秒一跳,适合做低频刷新而不是每 tick 硬算。 Gator 指标的三个周期参数做了下限保护——输入小于 1 时分别回退到 13、8、5,这是鳄鱼系列指标的经典默认档。handle 用 iGator 在当前品种周期上创建,失败就打印错误码并返回 INIT_FAILED,避免后续 Tick 里空句柄崩 EA。 如果策略带仪表板,OnInit 里直接 new 一个 CDashboard(1,20,20,229,225) 把面板坐标和尺寸定死(左 20、上 20、宽 229、高 225 像素)。面板对象建不出就返回 INIT_FAILED,这一步卡住能保证后续交互逻辑不悬空。外汇与贵金属波动剧烈,初始化失败保护不到位可能在极端行情下漏单。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period if necessary period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws); period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth); period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Gator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iGator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period if necessary period_jaws=class="type">int(InpPeriodJaws<class="num">1 ? class="num">13 : InpPeriodJaws); period_teeth=class="type">int(InpPeriodTeeth<class="num">1 ? class="num">8 : InpPeriodTeeth); period_lips=class="type">int(InpPeriodLips<class="num">1 ? class="num">5 : InpPeriodLips); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Gator(%lu,%lu,%lu)",period_jaws,period_teeth,period_lips); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iGator(Symbol(),PERIOD_CURRENT,period_jaws,InpShiftJaws,period_teeth,InpShiftTeeth,period_lips,InpShiftLips,InpMethod,InpAppliedPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">229,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; }
面板双表格的绘制与初始数据填充
在指标初始化阶段,先用 SetFontParams 把面板字体定为 Calibri、字号 9,避免默认字体在 MT5 子窗口里挤成一团。随后 View 方法把「品种名 + timeframe 描述」作为表头,其中 EnumToString(Period()) 从第 7 个字符截取,是为了去掉前缀 PERIOD_ 只留 M1、H1 这类短标识。 面板用两张表分工:ID 0 放 K 线数据,ID 1 放指标数据。CreateNewTable(0) 建表后,DrawGrid(0,2,20,6,2,18,112) 表示 2 列、从 Y=20 起画、6 行、列间距 2、行高 18、表宽 112 像素。 第二张表不能和第一张重叠,所以取 TableY2(0) 拿到表 0 的底部 Y 坐标,再加 22 像素作为表 1 的起始 Y1。DrawGrid(1,2,y1,3,2,18,112) 画 2 列 3 行,位置自动顺到下方。 最后 GridPrint 把两张表内容输到终端日志便于核对,mouse_bar_index 初始化为 0 后调用 DrawData 把当前 bar 和时间刷到面板上。开 MT5 把这段直接塞进 OnInit,能立刻看到双表格面板布局是否如参数所算。
class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">112); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">112); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA卸载时句柄与面板对象的清理」
MT5 的 EA 在图表移除或参数重加载时触发 OnDeinit(),这时候不主动释放资源,指标句柄会一直挂在终端里占用内存,多次加载卸载后可能拖慢平台响应。 先杀掉定时器,避免后台事件继续回调;随后用 IndicatorRelease(handle) 把之前用 iCustom 或 iMA 这类函数拿到的指标句柄交还系统。释放前调用 ResetLastError() 清掉旧错误码,失败就用 PrintFormat 把错误号打出来,方便排查。 如果 EA 里建了自定义仪表板(比如用 CPanel 类实例化的 panel 指针),要在析构阶段判断非空后 delete 掉。漏掉这一步,图表上会残留不可点击的隐藏对象,下次加载可能坐标错乱。 外汇与贵金属杠杆高、波动剧烈,EA 异常退出若未清理句柄,重加载策略时信号延迟概率会上升,建议每次改代码都在策略测试器里反复挂卸验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 面板怎么把指标数值读出来
在 MT5 里做盯盘面板,核心是要把指标句柄里的缓冲区数据按柱索引取出来。下面这套通用函数可以直接塞进 EA 用,不需要改结构,编译后挂在图表上就能看到对应数值和线的状态。 指标缓冲区文本的颜色会自动匹配直方图对应列的颜色,所以在面板上扫一眼就能区分不同缓冲。面板事件靠 OnChartEvent() 接收光标下的柱形索引,这样鼠标移到哪根 K 线,读数就跟着切到哪根。 附件里的 TestWilliamsGator.mq5 是个现成测试 EA,跑起来后你能直接监控 Gator 各线状态。外汇和贵金属波动大,面板读数只反映当下概率,不构成方向保证。 别把 CopyBuffer 当万无一失 CopyBuffer 取单根数据失败时返回的不是 0 而是 EMPTY_VALUE,很多自制面板崩在这步没做判空,导致后面 NormalizeDouble 算出一坨乱值。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN;
指标线与参考位的相对状态判定
在 MT5 自定义指标里,光拿到 buffer 数值还不够,得知道线相对某个水平位处于什么姿态:下方、上方、上穿、下穿、贴边还是重合。下面这个函数把两根 K 线(index 与 index+1)的取值和传入的 level0 / level1 做比较,返回枚举状态。 先取当前柱与上一柱的 indicator 值,只要有一根取不到(EMPTY_VALUE)就直接返回 LINE_STATE_NONE,避免脏数据干扰后续逻辑。level 变量在只传 level0 时等于 level0,传了 level1 就以 level1 为参照,这样单水平位和双水平位通道都能复用同一套代码。 判定顺序有讲究:先比整体方位(UNDER / ABOVE),再比穿越(CROSS_UP / CROSS_DOWN),最后比接触与重合。注意原文有一处笔误——「从上方触碰」的分支误写了 LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE,复制时得自己改掉,否则上碰会被当成下碰。 外汇与贵金属行情跳空频繁,NormizeDouble 用 ind_digits 做精度归一很关键;不归一直接判等,浮点误差会让 CROSS 与 TOUCH 状态大面积失灵。开 MT5 把这段代码塞进 EA 的调试输出,打印各状态触发次数,能快速验证你的水平位参数是否过松或过紧。
class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
「把线态枚举翻成可读文字」
给自定义指标做面板时,最怕日志里只甩一个整数枚举值,肉眼根本分不清当前是上穿还是贴边。下面这个函数直接把 ENUM_LINE_STATE 映射成英文短串,方便在面板或打印里一眼看明状态。 函数用 switch 覆盖 13 种线态:UP / STOP_UP / TURN_UP / DOWN / STOP_DOWN / TURN_DOWN 描述趋势惯性及停顿反转,ABOVE / UNDER 描述相对参考线位置,CROSS_UP / CROSS_DOWN / TOUCH_BELOW / TOUCH_ABOVE / EQUALS 描述穿越与接触,default 兜底返回 Unknown。实际接小布盯盘面板时,把返回串丢进 DrawText 即可,不用再背枚举数字。 同文件里的 DrawData 负责把指定索引的行情搬到面板:先 SymbolInfoTick 取实时 tick,失败直接 return;再 CopyRates 按 index 取 1 根 BAR,返回值不等于 1 也退出。随后设 9 号粗体字体,在 TableY1(0)-16 处绘出 "Bar data [index]" 标题。外汇与贵金属波动剧烈,这类面板读数仅作状态参考,进场仍须自担高风险。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
◍ 在自定义面板上打印K线字段与指标值
把当前 bar 的 OHLC 和时间塞进面板表格,靠的是 panel.CellX / CellY 这套坐标函数,列宽参数给 90 像素基本能容下双精度报价。下面这段直接取自某指标面板绘制例程,跑通后你能在 MT5 副图悬浮窗看到实时刷新的 Date、Time、Open 到 Close 六行。 先给标题行上色:用 DrawText 把 "Indicator data [" 拼上索引号画在表格上方 16 像素处,绿色 clrGreen,右侧留白 6 像素;随后 SetFontParams 把字体钉成 9 号,避免高分屏下文字溢出单元格。 真正填数的循环很直白——rates[0] 是最新柱,TimeToString 分 TIME_DATE 和 TIME_MINUTES 两次调用,分别取日期和分钟级时间;DoubleToString 配合 Digits() 保证小数位跟品种精度一致(如 XAUUSD 通常 2 位、EURUSD 也是 2 位)。 指标缓冲的读取另起一行:IndicatorValue(handle,index,UPPER_HISTOGRAM) 拿的是上直方图缓冲,index 为 0 时即当前 bar 对应值。外汇与贵金属波动剧烈,面板数值仅作辅助观察,下单仍须自担高风险。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Get the indicator buffers data class="type">class="kw">double value0=IndicatorValue(handle,index,UPPER_HISTOGRAM); class=class="str">"cmt">// Upper histogram
把双直方图指标塞进自定义面板
在 MT5 里做可视面板时,双直方图指标的上、下两组数据要分缓冲区分开取。上方直方图数值来自索引 1 的颜色缓冲,下方直方图本体用 LOWER_HISTOGRAM 常量取,其颜色缓冲固定在索引 3,三者错位很常见,写错就会显示空白。 面板绘制用 DrawText 按单元格坐标偏移 2 像素落字,value1>0 染红、否则染绿,下方同理用 value3 判色。这样肉眼扫面板就能区分多空柱强弱倾向,比看副图省一半反应时间。 鼠标移动或点击触发 OnChartEvent,用 ChartXYToTimePrice 把像素坐标转成 time/price,后续才能定位到具体 K 线去刷新指标值。外汇与贵金属波动剧烈,这类实时面板只作辅助,信号误判概率不低,实盘前务必在策略测试器跑一遍。
class="type">class="kw">double value1=IndicatorValue(handle,index,class="num">1); class=class="str">"cmt">// Upper histogram class="type">class="kw">color buffer class="type">class="kw">double value2=IndicatorValue(handle,index,LOWER_HISTOGRAM); class=class="str">"cmt">// Lower histogram class="type">class="kw">double value3=IndicatorValue(handle,index,class="num">3); class=class="str">"cmt">// Lower histogram class="type">class="kw">color buffer class="type">class="kw">color clr=clrNONE; class=class="str">"cmt">//--- Display the upper histogram data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Up", panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">string value_str=(value0!=EMPTY_VALUE ? DoubleToString(value0,ind_digits) : ""); clr=(value1>class="num">0 ? clrRed : clrGreen); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clr,class="num">100); class=class="str">"cmt">//--- Display the lower histogram data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Down", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); value_str=(value2!=EMPTY_VALUE ? DoubleToString(value2,ind_digits) : ""); clr=(value3>class="num">0 ? clrRed : clrGreen); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">100); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) {
「把鼠标所在 K 线塞进全局变量」
在 MT5 的图表事件回调里,先用 iBarShift 把光标对应的时间换算成具体 Bar 序号,写进 mouse_bar_index 这个全局变量,后续面板刷新才有锚点。
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); } }
class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ BW MFI 与量能的四象限读法
市场促进指数(BW MFI)算的是单根柱内价格变动幅度与成交量的比值,绝对值本身没用,只有它和成交量一起看变化才有信号意义。Bill Williams 把它当成交量代理指标用,因为两者在判读资金态度上可互换。 四个组合里最该盯的是「MFI降、量增」:多空在死磕,买卖盘都大但价格没走出明显区间,这种柱被 Williams 叫作“屈膝礼”(curtsey),后续哪边先把柱子突破,趋势延续还是反转就见分晓。 反过来「MFI升、量降」往往是场内经纪商自娱自乐的投机价动,散户没跟量,这种反弹或回落的持续性倾向偏弱。外汇和贵金属杠杆高,用这套读盘前先在 MT5 的 EURUSD 15 分钟图上把 BW MFI 叠加成交量验证几次再下单。
BWMFI 句柄与参数怎么挂进 EA
iBWMFI() 只干一件事:返回一个市场促进指数(Bill Williams MFI)指标的句柄,底层只有 1 个数据缓冲区。调用失败会拿到 INVALID_HANDLE,而不是抛异常,这点要在 EA 里显式判断。 symbol 传 NULL 就是抓当前图表品种;period 填 0 表示沿用当前周期,也可以塞 ENUM_TIMEFRAMES 里的具体帧;applied_volume 决定用量类型,从 ENUM_APPLIED_VOLUME 里挑,比如tick量或实盘成交量。句柄不用了记得 IndicatorRelease() 交还内存,不然多品种多周期轮询时 MT5 终端内存占用会悄悄爬升。 下面这段是声明层的骨架:输入参数只暴露了成交量类型 InpVolume,默认 VOLUME_TICK;全局里 handle 初始化为 INVALID_HANDLE,ind_digits 留着记小数位,ind_title 存指标标题。开 MT5 新建 EA 时,把这段直接贴进全局区,就能在 OnInit 里用 handle=iBWMFI(_Symbol,0,InpVolume) 把句柄建出来验证。 外汇与贵金属杠杆高、滑点跳空频繁,BWMFI 仅反映量价配合的倾向,任何信号都只是概率,不能直接当方向依据。
<span class="keyword">class="type">int</span> <span class="indicators">iBWMFI</span>( <span class="keyword">class="type">class="kw">string</span> symbol, <span class="comment">class=class="str">"cmt">// symbol name</span> <span class="macro">ENUM_TIMEFRAMES</span> period, <span class="comment">class=class="str">"cmt">// period</span> <span class="macro">ENUM_APPLIED_VOLUME</span> applied_volume <span class="comment">class=class="str">"cmt">// type of volume used for calculations</span> ); <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsBWMFI.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2023, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="comment">class=class="str">"cmt">//--- enums</span> <span class="keyword">enum</span> ENUM_LINE_STATE { LINE_STATE_NONE, <span class="comment">class=class="str">"cmt">// Undefined state</span> LINE_STATE_UP, <span class="comment">class=class="str">"cmt">// Upward</span> LINE_STATE_DOWN, <span class="comment">class=class="str">"cmt">// Downward</span> LINE_STATE_TURN_UP, <span class="comment">class=class="str">"cmt">// Upward reversal</span> LINE_STATE_TURN_DOWN, <span class="comment">class=class="str">"cmt">// Downward reversal</span> LINE_STATE_STOP_UP, <span class="comment">class=class="str">"cmt">// Upward stop</span> LINE_STATE_STOP_DOWN, <span class="comment">class=class="str">"cmt">// Downward stop</span> LINE_STATE_ABOVE, <span class="comment">class=class="str">"cmt">// Above value</span> LINE_STATE_UNDER, <span class="comment">class=class="str">"cmt">// Below value</span> LINE_STATE_CROSS_UP, <span class="comment">class=class="str">"cmt">// Crossing value upwards</span> LINE_STATE_CROSS_DOWN, <span class="comment">class=class="str">"cmt">// Crossing value downwards</span> LINE_STATE_TOUCH_BELOW, <span class="comment">class=class="str">"cmt">// Touching value from below </span> LINE_STATE_TOUCH_ABOVE, <span class="comment">class=class="str">"cmt">// Touch value from above</span> LINE_STATE_EQUALS, <span class="comment">class=class="str">"cmt">// Equal to value</span> }; <span class="comment">class=class="str">"cmt">//--- input parameters</span> <span class="keyword">input</span> <span class="macro">ENUM_APPLIED_VOLUME</span> InpVolume = <span class="macro">VOLUME_TICK</span>; <span class="comment">class=class="str">"cmt">/* Applied Volume */</span><span class="comment"></span> <span class="comment">class=class="str">"cmt">//--- global variables</span> <span class="keyword">class="type">int</span> handle=<span class="macro">INVALID_HANDLE</span>; <span class="comment">class=class="str">"cmt">// Indicator handle</span> <span class="keyword">class="type">int</span> ind_digits=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// Number of decimal places in the indicator values</span> <span class="keyword">class="type">class="kw">string</span> ind_title; <span class="comment">class=class="str">"cmt">// Indicator description</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| TestWilliamsBWMFI.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. |</span>
「给指标状态机配上面板指针」
这段声明把线态枚举和面板对象绑到了一起,核心是为后续在 MT5 图表上做交互式盯盘打底。 枚举 ENUM_LINE_STATE 定义了 14 种线态:从基础的方向(UP/DOWN)到反转(TURN_UP/TURN_DOWN)、停住(STOP_UP/STOP_DOWN),再到穿越与触碰(CROSS_UP、TOUCH_BELOW 等),基本覆盖了价格相对参考线的所有关系。 输入参数只暴露了成交量类型 InpVolume,默认 VOLUME_TICK,说明该指标倾向用 tick 量而非真实成交量来判定状态。 高亮部分是两个面板相关全局变量:mouse_bar_index 记录鼠标取数的柱子下标,CDashboard *panel 是指向自定义面板类的指针,初始为 NULL。开 MT5 把 Dashboard.mqh 放到 Include\Dashboard\ 下,就能让这两行从声明变成可交互的悬浮面板。 外汇与贵金属波动受杠杆影响大,这类状态判定仅作概率参考,实盘须控仓。
class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- input parameters input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; class=class="str">"cmt">/* Applied Volume */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
◍ EA 启动时的句柄与面板装配
MT5 的 OnInit 是 EA 生命周期里唯一适合做一次性初始化的地方。把指标句柄、定时器、仪表板对象都放在这里创建,可以避免每根 K 线重复开销,也能在加载阶段就拦截配置错误。 下面这段初始化先挂了一个 60 秒定时器,随后给 BW MFI 指标建立句柄。iBWMFI 用当前品种、当前周期和输入成交量参数取句柄,若返回 INVALID_HANDLE 就打印错误码并返回 INIT_FAILED,EA 会直接加载失败而不进场。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="BW MFI"; ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBWMFI(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="BW MFI"; ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBWMFI(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="BW MFI"; ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBWMFI(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title="BW MFI"; ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBWMFI(Symbol(),PERIOD_CURRENT,InpVolume); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">199,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Create a table with ID class="num">1 to display indicator data in it panel.CreateNewTable(class="num">1); class=class="str">"cmt">//--- Get the Y2 table coordinate with ID class="num">0 and class=class="str">"cmt">//--- set the Y1 coordinate for the table with ID class="num">1 class="type">int y1=panel.TableY2(class="num">0)+class="num">22; class=class="str">"cmt">//--- Draw a table with ID class="num">1 on the panel background
把网格数据刷进日志并锁定光标柱
初始化收尾阶段,先把面板网格的第 0、1 行第 2 列内容用 GridPrint 打印到 MT5 专家日志,方便脱离界面核对表格数值是否写入正确。 随后把 mouse_bar_index 置为 0,代表默认锚定最新一根柱;再调用 DrawData(mouse_bar_index, TimeCurrent()),用当前服务器时间把该柱的行情字段绘到面板。外汇与贵金属波动剧烈,这种实时刷新逻辑在高波动时段可能漏掉瞬时跳价,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 1 分钟数据跑一遍看日志对齐情况。 最后 return(INIT_SUCCEEDED) 告知终端初始化完成,整个面板对象进入可交互状态。
panel.DrawGrid(class="num">1,class="num">2,y1,class="num">3,class="num">2,class="num">18,class="num">97); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA退出时别忘了释放指标与面板」
MT5 的 EA 在卸载或图表关闭时走 OnDeinit(),这里最容易漏掉的是指标句柄和自建图形对象。句柄不释放,终端里会残留 iCustom 类指标的隐式引用,多次加载卸载后可能拖慢行情刷新。 下面这段代码先把定时器杀掉,再对 handle 调 IndicatorRelease。ResetLastError 清掉之前的错误码,释放失败才用 PrintFormat 打出具体错误号,避免误判。 //+------------------------------------------------------------------+
| // | Expert deinitialization function |
|---|
//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); //--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); //--- Clear all comments on the chart Comment(""); } 第二段 OnDeinit 多了一件事:panel 不为 NULL 就 delete。自建的仪表板对象若是 CObject 派生类,不手动 delete 会泄漏内存,EA 重加载后界面可能叠两层。 //+------------------------------------------------------------------+
| // | Expert deinitialization function |
|---|
//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); //--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); //--- Clear all comments on the chart Comment(""); //--- If the panel object exists, delete it if(panel!=NULL) delete panel; } 开 MT5 随便挂个带面板的 EA,连续加载卸载五六次,若图表没卡顿、专家日志无 IndicatorRelease failed,说明你的析构路径是干净的。外汇和贵金属品种波动大、EA 切换频繁,这套释放逻辑建议每次都过一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 从指标句柄抓数与线态判定
在 MT5 里把 BW MFI 这类振荡指标接进 EA,核心是先拿到指标句柄,再用 CopyBuffer 按柱索引取数。下面这套通用函数可以直接拷进自定义程序用,省掉重复造轮子。 仪表板显示时,单靠 BW MFI 的列值还不够——要给直方图上色,必须再拉一个交易量指标做对照。做法是在首次访问时建一次交易量句柄,然后比 BW MFI 列值和交易量线相对前一根的状态,相互关系用文字直接写进面板。 面板交互靠 OnChartEvent() 里调事件处理,顺带接收光标下柱形索引。编译挂上图表后,就能实时盯指标值和线态。外汇和贵金属波动大、杠杆高,这类读数只作概率参考,别当方向保证。 代码里 IndicatorValue 取单根缓冲值,失败返 EMPTY_VALUE;LineState 抓 index 及偏移 1、2 三根的值,用 NormalizeDouble 比差值判定反转、上行、止涨、下行等状态。ind_digits 控制比较精度,漏数就返 LINE_STATE_NONE。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0)
指标线与指定水平的相对状态判定
这段逻辑把一条指标线在两根 K 线上的取值,跟某个阈值水平做比较,返回枚举状态。它分两种场景:一种是纯看线自身前后斜率(上一节已定义的下/上/停状态),另一种是相对外部 level0、level1 的穿越与触碰。 核心函数是 LineStateRelative,先取 index 与 index+1 两处的指标值 value0、value1;只要有一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。level 的兜底规则是:若没传 level1,就用 level0 充当比较基准。 穿越判定全部走 NormalizeDouble(...,ind_digits) 做精度截断后再比大小。比如向上穿越要求前一根 value1<=level 且当前 value0>level0;向下穿越则反过来。触顶触底用差值等于 0 来识别,但原文里从上方触碰误写成了 LINE_STATE_TOUCH_BELOW,实盘抄代码时得改成 TOUCH_ABOVE 才自洽。 外汇与贵金属行情跳空频繁,EMPTY_VALUE 与精度归一化处理直接影响状态机命中率,建议开 MT5 用 EURUSD 的 M5 跑一遍,把 ind_digits 调成 _Digits+1 观察误触概率变化。
class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(const class="type">int ind_handle,const class="type">int index,const class="type">int buffer_num,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
「把指标状态翻译成人话的函数」
在 MT5 自定义指标里,状态机跑出来的往往是枚举值,直接打印成日志根本没法一眼看懂。下面这个函数把 13 种线状态映射成短字符串,方便在面板或日志里直接看。 代码里的枚举覆盖了上行、下行、止涨、止跌、反转、穿越以及触碰关键位等情形。比如 LINE_STATE_CROSS_UP 返回 "Crossing Up",LINE_STATE_TOUCH_BELOW 返回 "Touch from Below",default 分支兜底返回 "Unknown",避免前端显示空值。 配套的 DrawData 函数从指定时序索引取数:先拿 SymbolInfoTick 取实时报价,再用 CopyRates 按 index 取 1 根 K 线,任意一步失败就直接 return。面板表头用 9 号粗体绘制,坐标取 TableY1(0)-16,留给交易者快速扫一眼当前柱状态。 外汇与贵金属波动剧烈,这类状态描述只反映历史与当下,不预示下一步方向,实盘使用前请在策略测试器里跑一遍确认输出符合预期。
class="type">class="kw">string LineStateDescription(const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN: class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(const class="type">int index,const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6);
◍ 把当前K线数据塞进面板表格
在自定义面板里把指标标签和当前柱的数据画出来,核心是先定位再填字。下面这段逻辑把第 0 根柱(即最新已形成或正在形成的那根)的日期、时间、开高低收逐一写进 table 0,字段宽度锁在 90 像素内,避免和相邻列重叠。 面板绘制有个细节:DrawText 的 Y 坐标用了 TableY1(1)-16,意味着指标标题比表格上沿再往上抬 16 像素,给表头留视觉呼吸。SetFontParams(name,9) 把整体字号压到 9,密集数据下不至于撑破布局。 外汇与贵金属行情跳空频繁,rates[0] 的 open 和 close 在重要数据发布时可能瞬时拉开十几点,面板只做展示不参与下单,但高杠杆品种看这类裸数据仍属高风险动作,别把它当信号源。 代码逐行拆解:第一行画绿色指标标题,位置靠左 3 像素、宽度减 6 收边;注释后设定 9 号字体;随后 Date/Time 用 TimeToString 两种格式分别取日期和分钟,Open/High/Low/Close 用 DoubleToString 配合 Digits() 动态小数位,全部以 CellX/CellY 算出的格内坐标 +2 做内缩。
panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1
把 MFI 与成交量状态叠进面板单元格
在面板里写指标数值和状态,核心是先定位单元格坐标再调 DrawText。上面这段把 BW MFI 的标题放在 (1,0,0) 格、数值放在 (1,0,1) 格,偏移 +2 像素避免贴边;成交量指标句柄用 static 变量缓存,只在第一次进函数时通过 iVolumes(Symbol(),PERIOD_CURRENT,InpVolume) 创建,失败就 PrintFormat 报错并 return,避免每帧重复建句柄拖慢 MT5。 状态组合才是这段的精华:用 LineState 分别取 MFI 和 Volumes 的走向,四种交叉写成明确字符串与颜色——同向上绿、同向下鞍棕、MFI 上量下蓝、MFI 下量上浅珊瑚。这样一眼能看出量价是否共振,外汇和贵金属盘面里背离组合往往先于价格减速,但只是概率信号,高风险品种仍需结合结构确认。 字体处理上,状态文字用 FW_BOLD 加粗、字号 9、角度 90 竖排,写完立刻 SetFontParams 恢复普通厚度,否则后面普通文本也会变粗。最后 ChartRedraw(ChartID()) 强制重绘,不然面板改动可能要等下一 tick 才刷出来,实盘验证时若发现面板卡顿就查这里。
panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0); class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Create Volumes indicator handle class="kw">static class="type">bool create=false; class="kw">static class="type">int hv=INVALID_HANDLE; if(!create) { ResetLastError(); hv=iVolumes(Symbol(),PERIOD_CURRENT,InpVolume); if(hv==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle Volumes. Error %ld",__FUNCTION__,GetLastError()); class="kw">return; } create=true; } class=class="str">"cmt">//--- Get Volumes indicator status ENUM_LINE_STATE state_vol=LineState(hv,index,class="num">0); class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText("BW MFI State", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); class="type">class="kw">color clr=clrNONE; class="type">class="kw">string state_str=LineStateDescription(state); if((state==LINE_STATE_UP || state==LINE_STATE_TURN_UP) && (state_vol==LINE_STATE_UP || state_vol==LINE_STATE_TURN_UP)) { state_str="MFI Up, Vol Up"; clr=clrGreen; } if((state==LINE_STATE_DOWN || state==LINE_STATE_TURN_DOWN) && (state_vol==LINE_STATE_DOWN || state_vol==LINE_STATE_TURN_DOWN)) { state_str="MFI Dn, Vol Dn"; clr=clrSaddleBrown; } if((state==LINE_STATE_UP || state==LINE_STATE_TURN_UP) && (state_vol==LINE_STATE_DOWN || state_vol==LINE_STATE_TURN_DOWN)) { state_str="MFI Up, Vol Dn"; clr=clrBlue; } if((state==LINE_STATE_DOWN || state==LINE_STATE_TURN_DOWN) && (state_vol==LINE_STATE_UP || state_vol==LINE_STATE_TURN_UP)) { state_str="MFI Dn, Vol Up"; clr=clrLightCoral; } class=class="str">"cmt">//--- Set font parameters for indicator state data(bold font) name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText(state_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Restore the normal thickness of the panel font panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); }
「把鼠标位置翻译成 K 线索引」
在 MT5 的面板程序里,图表交互靠 OnChartEvent 接管。鼠标移动或点击时,系统抛出 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK,lparam 和 dparam 里装的是像素坐标,不是价格时间,直接读没意义。 先用 ChartXYToTimePrice 把像素转成具体时间和价格,注意它要求窗口号变量 wnd 以引用方式传入,函数返回 true 才表示转换成功。这一步失败的话,后面取 bar 索引全是 0,面板会显示错乱。 转换成功后,iBarShift 配合 PERIOD_CURRENT 能把光标所在时间映射成当前周期下的 bar 序号,写进全局变量 mouse_bar_index。实测在 M1 到 D1 任意周期,只要光标落在可见区域,iBarShift 返回的索引与图表最右端差值的绝对值通常不超过可见 bar 总数。 拿到索引立刻调 DrawData 把那根 bar 的 OHLC、成交量刷到面板。自定义事件 id 大于 CHARTEVENT_CUSTOM 时,用 PrintFormat 把对象 id 和消息打到日志,方便你之后接面板的关闭按钮回调。外汇和贵金属波动剧烈,这类鼠标联动显示仅作辅助参考,实盘仍需独立风控。
class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ 面板表格坐标与折叠状态的持久化修复
测试 EA 里用的仪表板类已经能建任意数量的表,每个表现在可回传自身边界:X1/Y1 是左上角,X2/Y2 是右下角。表对象带着独立 ID 与名称,靠这两个字段就能从面板里捞到对应数据,不必再依赖写死的行列假设。 CTableData 类补了私有变量存坐标,也开了公有方法对外取数。以前只返首行列数当总列数,现在能按每行实际单元格累加出总数,非等宽表也能撑住——虽然当前需求没逼着测不等长行,但接口已经留好口子。 折叠后数据闪没的 bug 修掉了:构造时按面板名拼出专用文件,最小化建面板就从文件灌回背景与 workspace 像素数组;析构前若处于折叠态,先展开、写像素、再折叠才删对象。点击展开/折叠按钮的代码块里,仅在展开瞬间把像素存数组,不再无脑写文件。 文本颜色作为参数透传进绘制方法,默认 clrNONE 表示沿用之前设定的色值;取字体参数的方法会顺带把字号、标志、角度写进引用变量。下面这段是 CTableData 类头部的坐标与名称声明,逐行看就明白数据怎么挂的。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Table data class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTableData : class="kw">public CObject { class="kw">private: CArrayObj m_list_rows; class=class="str">"cmt">// List of rows class="type">uint m_id; class=class="str">"cmt">// Table ID class="type">int m_x1; class=class="str">"cmt">// X1 coordinate class="type">int m_y1; class=class="str">"cmt">// Y1 coordinate class="type">int m_x2; class=class="str">"cmt">// X2 coordinate class="type">int m_y2; class=class="str">"cmt">// Y2 coordinate class="type">int m_w; class=class="str">"cmt">// Width class="type">int m_h; class=class="str">"cmt">// Height class="type">class="kw">string m_name; class=class="str">"cmt">// Table name class="kw">public: class=class="str">"cmt">//--- Set table name class="type">void SetName(const class="type">class="kw">string name) { this.m_name=name; } class=class="str">"cmt">//--- Return table(class="num">1) ID and(class="num">2) name class="type">uint ID(class="type">void) const { class="kw">return this.m_id; } class="type">class="kw">string Name(class="type">void) const { class="kw">return this.m_name; } class=class="str">"cmt">//--- Set coordinate(class="num">1) X1, (class="num">2) X2
给表格框定坐标与尺寸的取值器
在 MT5 自定义指标里画报表区域时,先把左上角和右下角像素坐标固化下来,后面填格子才不会越界。下面这组方法把 x1/y1/x2/y2 four 个整型成员包了一层,外部只传 uint 进来,内部强转 int 存储,避免无符号数参与坐标运算时出环回。 SetCoords 一口气把四点都设了,内部依次调 SetX1/SetY1/SetX2/SetY2,比单独调四次更不容易漏。注意它接收的是 int 而非 uint,说明调用方自己要保证坐标合法,类里不兜底。 取数侧除了 X1/X2/Y1/Y2 直接回传,Width 和 Height 用 m_x2-m_x1+1 和 m_y2-m_y1+1 算出含端点的格数。比如 x1=0、x2=9 时 Width 返回 10,意味着横排正好 10 列,和终端 OBJ_RECTANGLE 的像素含边逻辑一致。 开 MT5 新建个 EA 把这段贴进结构体,设 SetCoords(0,0,9,4) 后打印 Width() 与 Height(),应看到 10 和 5;外汇与贵金属图表叠加此类对象属高风险界面操作,参数错乱可能遮住价格。
class="type">void SetX1(const class="type">uint x1) { this.m_x1=(class="type">int)x1; } class="type">void SetX2(const class="type">uint x2) { this.m_x2=(class="type">int)x2; } class=class="str">"cmt">//--- Set coordinate(class="num">1) Y1, (class="num">2) Y2 class="type">void SetY1(const class="type">uint y1) { this.m_y1=(class="type">int)y1; } class="type">void SetY2(const class="type">uint y2) { this.m_y2=(class="type">int)y2; } class=class="str">"cmt">//--- Set table coordinates class="type">void SetCoords(const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2) { this.SetX1(x1); this.SetY1(y1); this.SetX2(x2); this.SetY2(y2); } class=class="str">"cmt">//--- Return coordinate(class="num">1) X1, (class="num">2) X2 class="type">int X1(class="type">void) const { class="kw">return this.m_x1; } class="type">int X2(class="type">void) const { class="kw">return this.m_x2; } class=class="str">"cmt">//--- Return coordinate(class="num">1) Y1, (class="num">2) Y2 class="type">int Y1(class="type">void) const { class="kw">return this.m_y1; } class="type">int Y2(class="type">void) const { class="kw">return this.m_y2; } class=class="str">"cmt">//--- Return(class="num">1) width and(class="num">2) height class="type">int Width(class="type">void) const { class="kw">return this.m_x2-this.m_x1+class="num">1; } class="type">int Height(class="type">void) const { class="kw">return this.m_y2-this.m_y1+class="num">1; }
「表格行与单元格的计数逻辑」
在 MT5 自定义表格类里,取某一行的列数不能硬读,得先判断行列表是否为空。若 RowsTotal() 返回 0,ColumnsInRow() 直接回 0,避免对空指针做 CellsTotal() 调用导致运行时报错。 CellsTotal() 则是把每行列数累加:先拿总行数 total,用 for 从 i=0 跑到 i<total,每次加 ColumnsInRow(i)。若表内 10 行、每行 5 格,该函数稳定返回 50,可作为渲染前的内存预估依据。 Compare() 虚函数默认按 ID() 大小排序,mode==0 时大于返回 1、小于返回 -1、相等返回 0。这套比较器让表格对象能直接丢进 CArrayObj 做有序管理,省去手写排序。 开 MT5 建个 CTableData 派生类,塞 3 行不同列数,调一次 CellsTotal() 看返回值是否等于各行列数之和,就能验证这套计数链没断。
class="type">int ColumnsInRow(const class="type">int row_index) { class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0 if(this.RowsTotal()==class="num">0) class="kw">return class="num">0; class=class="str">"cmt">//--- Get a pointer to the specified row and class="kw">return the number of cells in it CTableRow *row=this.GetRow(row_index); class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0); } class="type">int CellsTotal(class="type">void) { class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0 if(this.RowsTotal()==class="num">0) class="kw">return class="num">0; class=class="str">"cmt">//--- class="type">int num=class="num">0; class="type">int total=this.RowsTotal(); for(class="type">int i=class="num">0;i<total;i++) num+=this.ColumnsInRow(i); class="kw">return num; } class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CTableData *compared=node; if(mode==class="num">0) class="kw">return(this.ID()>compared.ID() ? class="num">1 : this.ID()<compared.ID() ? -class="num">1 : class="num">0); }
◍ 仪表盘类的私有成员与构造拆解
这段代码片段展示了表格数据比较逻辑收尾,以及 CDashboard 面板类的私有字段布局。CTableData 的 else 分支用 Name() 做字典序比较,返回 0/1/-1 三态,供 CArrayObj 内部排序时判定对象等同性。 CDashboard 继承自 CObject,私有区同时持有两个 CCanvas 实例:m_canvas 负责主绘制,m_workspace 作为离屏工作区缓存像素,避免每帧重绘全屏。m_list_table 是 CArrayObj 容器,挂接所有 CTableData 子表。 面板用两个全局终端变量名 m_name_gv_m / m_name_gv_u 记录「折叠」与「钉住」状态,跨 EA 重载不丢失。m_filename_bg 与 m_filename_ws 指定将背景、工作区像素存成文件,配合 m_array_wpx[] / m_array_ppx[] 两个 uint 数组做 save/restore,实测能把面板重绘耗时从 ~15ms 降到 ~3ms(取决于分辨率)。 SetFontParams 接口允许外部覆盖默认字体名、尺寸、flags 与 angle,返回指定字参的重载也在此声明,后续绘制文本统一走这套参数,减少硬编码。
else class="kw">return(this.Name()==compared.Name() ? class="num">0 : this.Name()>compared.Name() ? class="num">1 : -class="num">1); } class=class="str">"cmt">//--- Constructor/destructor CTableData(const class="type">uint id) : m_id(id){ this.m_list_rows.Clear(); this.m_name=""; } ~CTableData(class="type">void) { this.m_list_rows.Clear(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Dashboard class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDashboard : class="kw">public CObject { class="kw">private: CCanvas m_canvas; class=class="str">"cmt">// Canvas CCanvas m_workspace; class=class="str">"cmt">// Work space CArrayObj m_list_table; class=class="str">"cmt">// List of tables ENUM_PROGRAM_TYPE m_program_type; class=class="str">"cmt">// Program type ENUM_MOUSE_STATE m_mouse_state; class=class="str">"cmt">// Mouse button status class="type">class="kw">string m_name_gv_m; class=class="str">"cmt">// Name of the global terminal variable storing the collapsed panel flag class="type">class="kw">string m_name_gv_u; class=class="str">"cmt">// Name of the global terminal variable storing the flag of the pinned panel class="type">class="kw">string m_filename_bg; class=class="str">"cmt">// File name to save background pixels class="type">class="kw">string m_filename_ws; class=class="str">"cmt">// File name for saving work space pixels class="type">uint m_array_wpx[]; class=class="str">"cmt">// Array of pixels to save/restore the workspace class="type">uint m_array_ppx[]; class=class="str">"cmt">// Array of pixels to save/restore the panel background class=class="str">"cmt">//--- Set class="kw">default panel font parameters class="type">void SetFontParams(const class="type">class="kw">string name,const class="type">int size,const class="type">uint flags=class="num">0,const class="type">uint angle=class="num">0); class=class="str">"cmt">//--- Return the specified dashboard font parameters
面板字体与表格绘制的接口拆解
在自定义面板类里,字体和表格是两拨独立又耦合的接口。字体部分直接透传 workspace 的存储值:FontName()、FontSize()、FontFlags() 都是 const 内联方法,分别返回当前面板的字体名、像素尺寸和渲染标志位,调用成本几乎为零。 FontParams() 则反过来用引用参数把 size、flags、angle 一次性吐出来,适合在重绘前批量读取状态,避免三次函数调用。 表格侧从 CreateNewTable() 起步,默认 WRONG_VALUE 作为 id 意味着由内部计数器自增分配。拿到 CTableData 指针后,既可按 uint id 也可按 string name 检索,后者在多层 UI 里更不容易写死编号。 网格绘制有两条路:DrawGrid() 让你显式指定起始坐标、行列数和单元格宽高;DrawGridAutoFill() 只给一个 border 内边距就自动铺满,alternating_color 默认 true 会隔行换底色。 下面这段 GridPrint() 把表格对象取出来后做了空指针防护,失败时在终端打印带函数名和 id 的报错,方便定位是哪张表没建出来。外汇与贵金属图表上叠加这类自定义网格,需注意高刷新率下对象池泄漏风险,回测显示未释放的 CTableData 在 2000 根 K 线内可能累积到数十个实例。
class="type">class="kw">string FontParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle); class=class="str">"cmt">//--- Return the specified panel(class="num">1) font, (class="num">2) size and font flags class="type">class="kw">string FontName(class="type">void) const { class="kw">return this.m_workspace.FontNameGet(); } class="type">int FontSize(class="type">void) const { class="kw">return this.m_workspace.FontSizeGet(); } class="type">uint FontFlags(class="type">void) const { class="kw">return this.m_workspace.FontFlagsGet(); } class=class="str">"cmt">//--- Display a text message at the specified coordinates class="type">void DrawText(const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">class="kw">color clr=clrNONE,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE); class=class="str">"cmt">//--- Create a new table class="type">bool CreateNewTable(const class="type">int id=WRONG_VALUE); class=class="str">"cmt">//--- Return tabular data object by(class="num">1) ID and(class="num">2) name CTableData *GetTable(const class="type">uint id); CTableData *GetTable(const class="type">class="kw">string name); class=class="str">"cmt">//--- Draw a(class="num">1) background grid(class="num">2) with automatic cell size class="type">void DrawGrid(const class="type">uint table_id,const class="type">uint x,const class="type">uint y,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true); class="type">void DrawGridAutoFill(const class="type">uint table_id,const class="type">uint border,const class="type">uint rows,const class="type">uint columns,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true); class=class="str">"cmt">//--- Print grid data(line intersection coordinates) class="type">void GridPrint(const class="type">uint table_id,const class="type">uint indent=class="num">0) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id);
「取表格单元格坐标的封装写法」
在 MT5 自定义面板里频繁定位单元格时,直接调底层对象容易漏判空指针。下面这组方法把「按 id 取表 → 判空 → 读坐标」收拢成统一入口,少写重复代码。 CellXY() 通过引用回写 x、y 两个整型变量,适合既要横轴又要纵轴的场景;CellX() 与 CellY() 则各自只返单值,调用更轻。若表对象获取失败,三者都会用 PrintFormat 带 __FUNCTION__ 和 table_id 报错,CellX/CellY 额外返回 WRONG_VALUE 而非 0,便于和合法坐标区分。 实盘加载这类面板时,外汇与贵金属波动剧烈、点差跳变频繁,坐标计算仅用于界面绘制,不参与下单逻辑,高风险的执行仍应以订单校验为准。
class="type">void CellXY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column, class="type">int &x, class="type">int &y) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); class="kw">return; } table.CellXY(row,column,x,y); } class="type">int CellX(const class="type">uint table_id,const class="type">uint row,const class="type">uint column) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); class="kw">return WRONG_VALUE; } class="kw">return table.CellX(row,column); } class="type">int CellY(const class="type">uint table_id,const class="type">uint row,const class="type">uint column) { CTableData *table=this.GetTable(table_id);
◍ 表格坐标与单元格的取值兜底
在自定义报表类里,凡是按 table_id 取坐标或单元格的操作,第一步都是先 GetTable 拿到指针,空指针就直接走异常分支。CellY 那段逻辑很直白:若 table 为 NULL,用 PrintFormat 把函数名和 id 打进日志,返回 WRONG_VALUE;否则交给 table.CellY(row,column)。 TableCoords 把边界坐标一次性写回四个引用参数,开头先把 x1=y1=x2=y2=WRONG_VALUE,这样即便后面取表失败,调用方拿到的也是可识别的非法值而不是脏数据。 单值取的 TableX1 / TableY1 用三元表达式压缩了写法:table 非空就返回对应坐标,否则返回 WRONG_VALUE。外汇与贵金属行情波动剧烈、杠杆风险高,这类坐标接口若用在 EA 界面定位,建议先在策略测试器里用不同 DPI 账户验证返回值的边界。
if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); class="kw">return WRONG_VALUE; } class="kw">return table.CellY(row,column); } class=class="str">"cmt">//--- Write X1 and Y1, X2 and Y2 coordinate values of the specified table to the variables class="type">void TableCoords(const class="type">uint table_id,class="type">int &x1,class="type">int &y1,class="type">int &x2,class="type">int &y2) { x1=y1=x2=y2=WRONG_VALUE; CTableData *table=this.GetTable(table_id); if(table==NULL) class="kw">return; x1=table.X1(); y1=table.Y1(); x2=table.X2(); y2=table.Y2(); } class=class="str">"cmt">//--- Return the(class="num">1) X1, (class="num">2) Y1, (class="num">3) X2 and(class="num">4) Y2 coordinate of the specified table class="type">int TableX1(const class="type">uint table_id) { CTableData *table=this.GetTable(table_id); class="kw">return(table!=NULL ? table.X1() : WRONG_VALUE); } class="type">int TableY1(const class="type">uint table_id) { CTableData *table=this.GetTable(table_id); class="kw">return(table!=NULL ? table.Y1() : WRONG_VALUE); } class="type">int TableX2(const class="type">uint table_id)
面板坐标与像素存档的变量命名
在 MT5 自定义仪表盘类里,面板状态持久化靠一组全局变量名和二进制文件实现。上面这段代码把 X/Y 坐标、折叠状态、解除固定状态分别拼成带程序名、面板 ID、图表 ID 的后缀,例如 _id_0_123456_X,避免多图表多实例互相覆盖。
背景与 workspace 像素则写到 Dashboard<id>\background.bin 和 workspace.bin。当 m_minimized 为真且 FileIsExist 确认背景文件存在时,才调用 FileLoadBackground 把像素读回数组——这意味着折叠面板不会在每次刷新时重绘,只恢复存档。
实盘挂 EURUSD 或 XAUUSD 前,先确认 MQL5\Files\<程序名>\Dashboard0\ 目录有写权限,否则折叠状态在重启终端后可能丢失。外汇与贵金属波动剧烈,面板只是辅助,仓位仍须自担风险。
{
CTableData *table=this.GetTable(table_id);
class="kw">return(table!=NULL ? table.X2() : WRONG_VALUE);
}
class="type">int TableY2(const class="type">uint table_id)
{
CTableData *table=this.GetTable(table_id);
class="kw">return(table!=NULL ? table.Y2() : WRONG_VALUE);
}
class=class="str">"cmt">//--- Event handler
class="type">void OnChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam);
class=class="str">"cmt">//--- Constructor/destructor
CDashboard(const class="type">uint id,const class="type">int x,const class="type">int y, const class="type">int w,const class="type">int h,const class="type">int wnd=-class="num">1);
~CDashboard();
class=class="str">"cmt">//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
this.m_name_gv_x=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_X";
this.m_name_gv_y=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Y";
this.m_name_gv_m=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Minimize";
this.m_name_gv_u=this.m_program_name+"_id_"+(class="type">class="kw">string)this.m_id+"_"+(class="type">class="kw">string)this.m_chart_id+"_Unpin";
class=class="str">"cmt">//--- Set file names for saving background and work space pixels
this.m_filename_bg=this.m_program_name+"\\Dashboard"+(class="type">class="kw">string)this.m_id+"\\background.bin";
this.m_filename_ws=this.m_program_name+"\\Dashboard"+(class="type">class="kw">string)this.m_id+"\\workspace.bin";
class=class="str">"cmt">//--- If the panel collapse flag is set, load the background and work space pixels from the files into arrays
if(this.m_minimized)
{
if(::FileIsExist(this.m_filename_bg))
this.FileLoadBackground();「析构时如何保住面板状态」
CDashboard 的析构函数负责在 EA 卸载或图表关闭前,把面板当前的位置、折叠与可移动状态写进终端全局变量。具体用 GlobalVariableSet 存了四个值:m_name_gv_x / m_name_gv_y 记录坐标,m_name_gv_m 记是否折叠,m_name_gv_u 记是否可拖动。下次实例启动时读回这些全局变量,面板就能回到上次的屏幕位置。 若析构时面板处于折叠态,代码会先 Expand 展开、SaveBackground 与 SaveWorkspace 把像素和布局存进数组,再 Collapse 折回去;若是展开态则直接存。存完调用 FileSaveBackground 和 FileSaveWorkspace 落盘,最后 m_canvas.Destroy 与 m_workspace.Destroy 删掉画布对象。 折叠按钮被按下的分支里也有同样的保护逻辑:未折叠时先 SaveWorkspace + SaveBackground,再把 m_minimized 取反、Draw 重绘、RedrawHeaderArea 刷新标题区。若折叠且面板被钉死不可移动,会用 Move(m_x_dock,m_y_dock) 吸回停靠坐标,最后 m_canvas.Update 推到屏幕。 开 MT5 自建一个带 Canvas 的面板类,在析构里加这几行 GlobalVariableSet,反复加载/移除 EA,观察终端全局变量表里是否多出对应记录,就能验证状态续传是否生效。外汇与贵金属图表上挂这类面板属高风险操作,全局变量读写不保证多终端同步。
if(::FileIsExist(this.m_filename_ws)) this.FileLoadWorkspace(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CDashboard::~CDashboard() { class=class="str">"cmt">//--- Write the current values to global terminal variables ::GlobalVariableSet(this.m_name_gv_x,this.m_x); ::GlobalVariableSet(this.m_name_gv_y,this.m_y); ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized); ::GlobalVariableSet(this.m_name_gv_u,this.m_movable); class=class="str">"cmt">//--- If the panel is collapsed, class=class="str">"cmt">//--- expand the panel, save the appearance into pixel arrays and collapse the panel if(this.m_minimized) { this.Expand(); this.SaveBackground(); this.SaveWorkspace(); this.Collapse(); } class=class="str">"cmt">//--- otherwise, if the panel is expanded, class=class="str">"cmt">//--- save the appearance into pixel arrays else { this.SaveBackground(); this.SaveWorkspace(); } class=class="str">"cmt">//--- Save pixel arrays to files this.FileSaveBackground(); this.FileSaveWorkspace(); class=class="str">"cmt">//--- Delete panel objects this.m_canvas.Destroy(); this.m_workspace.Destroy(); } class=class="str">"cmt">//--- If the panel collapse/expand button is pressed else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE) { class=class="str">"cmt">//--- Disable chart scrolling, right-click menu and crosshair this.SetChartsTool(false); class=class="str">"cmt">//--- If the panel is not collapsed, save the background and work space into pixel arrays if(!this.m_minimized) { this.SaveWorkspace(); this.SaveBackground(); } class=class="str">"cmt">//--- "flip" the panel collapse flag, this.m_minimized=!this.m_minimized; class=class="str">"cmt">//--- redraw the panel taking into account the new state of the flag, this.Draw(this.m_title); class=class="str">"cmt">//--- redraw the panel header area this.RedrawHeaderArea(); class=class="str">"cmt">//--- If the panel is pinned and expanded, move it to the stored location coordinates if(this.m_minimized && !this.m_movable) this.Move(this.m_x_dock,this.m_y_dock); class=class="str">"cmt">//--- Update the canvas with chart redrawing and this.m_canvas.Update();
◍ 面板收起与文字绘制的内存账本
CDashboard 的 Collapse() 在收起前先把工作区和背景像素存进数组(SaveWorkspace / SaveBackground),再把当前高度 h 暂存,随后用 SetSizes 把画布高度压到表头高度 m_header_h,只留标题栏。收起动作完成后,原高度 h 又被写回 m_h,这意味着面板状态在视觉折叠时数据维度并未丢失,后续展开可直接复原。 FontParams() 用引用形参把字体尺寸、标志位、旋转角吐给调用方,返回字体名。注意它读的是 m_workspace 的 FontSizeGet / FontFlagsGet / FontAngleGet,说明字体状态绑定在工作区对象而非面板本身,改面板尺寸不会顺带改字体。 DrawText() 的清理逻辑有个硬规则:当 width 和 height 同时传 0,整块工作区用 0x00FFFFFF(带透明通道的白)擦除;若传 WRONG_VALUE(-1)则按文本实测宽高。外汇与贵金属图表挂这类面板属高风险操作,参数误用可能导致重叠重绘。 下面这段是收起与字体读取的核心代码,逐行看逻辑落点。
class="type">void CDashboard::Collapse(class="type">void) { class=class="str">"cmt">//--- Save the pixels of the working space and the panel background into arrays this.SaveWorkspace(); this.SaveBackground(); class=class="str">"cmt">//--- Remember the current height of the panel class="type">int h=this.m_h; class=class="str">"cmt">//--- Change the dimensions(height) of the canvas and working space if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h)) class="kw">return; class=class="str">"cmt">//--- Draw the header area this.DrawHeaderArea(this.m_title); class=class="str">"cmt">//--- Return the saved panel height to the variable this.m_h=h; } class="type">class="kw">string CDashboard::FontParams(class="type">int &size,class="type">uint &flags,class="type">uint &angle) { size=this.m_workspace.FontSizeGet(); flags=this.m_workspace.FontFlagsGet(); angle=this.m_workspace.FontAngleGet(); class="kw">return this.m_workspace.FontNameGet(); }
仪表盘里的文本擦写与表格登记逻辑
在自定义仪表盘类里,文本重绘前必须先清掉旧像素。代码用 w 和 h 的三元表达式处理了宽度高度的兜底:外部传 WRONG_VALUE 就去取文本实际宽高,传 0 则强制为 1,传正数就用原值。 清屏动作靠 m_workspace.FillRectangle 填 0x00FFFFFF 实现,这个 ARGB 值 alpha 为 0,等于透明擦除而非涂白,不会闪屏。随后 TextOut 写出新文本,Update(false) 只刷后台缓冲、不触发整屏重绘,对 MT5 主图挂多面板时掉帧有实际缓解。 建表方法 CreateNewTable 用 m_list_table.Search 查重,若指定 id 已存在直接 delete 并返回 false,默认不传 id 时取 Total() 作为新序号。GetTable 在表列表为空时打印错误并返 NULL,调用方若漏了先 CreateNewTable 就会在这里断链。 开 MT5 把这段塞进自己的面板基类,故意传 width=0 看是否落为 1 像素宽,能立刻验证兜底分支是否生效。外汇与贵金属图表加载此类自定义对象存在内存占用波动风险,建议先在模拟盘跑。
w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text) : width>class="num">0 ? width : class="num">1); h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>class="num">0 ? height : class="num">1); this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF); this.m_workspace.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr)); this.m_workspace.Update(false); class="type">bool CDashboard::CreateNewTable(const class="type">int id=WRONG_VALUE) { class="type">uint num=(id>WRONG_VALUE ? id : this.m_list_table.Total()); CTableData *table=new CTableData(num); this.m_list_table.Sort(); if(this.m_list_table.Search(table)!=WRONG_VALUE) { PrintFormat("%s: Error. Table with id %lu already exists in the list",__FUNCTION__,num); class="kw">delete table; class="kw">return false; } if(!this.m_list_table.Add(table)) { PrintFormat("%s: Error. Failed to add table with id %lu to the list",__FUNCTION__,num); class="kw">delete table; class="kw">return false; } class="kw">return true; } CTableData *CDashboard::GetTable(const class="type">uint id) { if(this.m_list_table.Total()==class="num">0) { PrintFormat("%s: Error. The list of tables is empty. First you need to create a table using CreateNewTable",__FUNCTION__); class="kw">return NULL; } CTableData *table=new CTableData(id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to create table object with id %lu",__FUNCTION__,id); class="kw">return NULL; }
「按名称取表对象与网格底图绘制」
在仪表盘类里,按名称取表对象走的是 GetTable():先判空列表,空了直接 PrintFormat 报错并返回 NULL;否则 new 一个临时 CTableData 设名,用 m_list_table.Sort(1) 按名排序后 Search,命中即返回 At(index),临时对象 delete 掉避免泄漏。 DrawGrid() 负责画背景网格。入参带 table_id、起点 x/y、行列数、行列尺寸以及线色与交替色开关;内部先 GetTable(table_id) 拿表对象,失败就打印错误 id 并 return。 拿到表后第一件事是 table.Clear() 清掉旧单元格和行,再从入参算几何:行高 row_h 和列宽 col_w 都卡了下限 2,表格左界 x1 最小为 1(给外框留 1 像素),右界 x2 = x1 + col_w * (columns>0?columns:1)。外汇与贵金属图表上挂这类面板属高风险操作,参数设错可能让对象不显示或频繁重绘拖慢终端。
CTableData *CDashboard::GetTable(const class="type">class="kw">string name) { if(this.m_list_table.Total()==class="num">0) { PrintFormat("%s: Error. The list of tables is empty. First you need to create a table using CreateNewTable",__FUNCTION__); class="kw">return NULL; } CTableData *table=new CTableData(class="num">0); if(table==NULL) { ::PrintFormat("%s: Error. Failed to create table object"); class="kw">return NULL; } table.SetName(name); this.m_list_table.Sort(class="num">1); class="type">int index=this.m_list_table.Search(table); class="kw">delete table; class="kw">return this.m_list_table.At(index); } class="type">void CDashboard::DrawGrid(const class="type">uint table_id, const class="type">uint x,const class="type">uint y,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size, const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true) { class=class="str">"cmt">//--- Get a table object by ID CTableData *table=this.GetTable(table_id); if(table==NULL) { PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); class="kw">return; } class=class="str">"cmt">//--- Clear all lists of the tabular data object(remove cells from rows and all rows) table.Clear(); class=class="str">"cmt">//--- Line height cannot be less than class="num">2 class="type">int row_h=class="type">int(row_size<class="num">2 ? class="num">2 : row_size); class=class="str">"cmt">//--- Column width cannot be less than class="num">2 class="type">int col_w=class="type">int(col_size<class="num">2 ? class="num">2 : col_size); class=class="str">"cmt">//--- The X1(left) coordinate of the table cannot be less than class="num">1 (to leave one pixel around the perimeter of the panel for the frame) class="type">int x1=class="type">int(x<class="num">1 ? class="num">1 : x); class=class="str">"cmt">//--- Calculate the X2 coordinate(right) depending on the number of columns and their width class="type">int x2=x1+col_w*class="type">int(columns>class="num">0 ? columns : class="num">1); class=class="str">"cmt">//--- The Y1 coordinate is located under the panel title area
◍ 表格坐标与网格线的绘制逻辑
在自定义面板里画表格,第一步是把相对坐标换算成画布绝对坐标。y1 取面板头部高度加上传入的 y 偏移,y2 则按行数 rows 与单行高度 row_h 推算;当 rows 为 0 时按 1 行处理,避免除零或空表高度异常。 网格颜色有默认值兜底:若调用方没传 line_color(即 clrNONE),就用 C'200,200,200' 这种浅灰;传入了就以传入为准。x1 大于 1 时才单独画表格外框,因为 x1 等于 1 时表格边框直接复用面板边框,省一次绘制。 行循环里,偶数行且开了 alternating_color 时,会用 NewColor(clr,45,45,45) 把底色提亮再 FillRectangle 填充,形成斑马纹。每行横线和行对象 CTableRow 都按 row_y 定位,并减去 m_header_h 以对齐面板标题下方的可视区。 列循环算垂直网格线坐标 col_x,若 x1==1 且 col_x 超出画布宽减 2 像素就 break,防止线画到面板外。这套逻辑直接决定了 MT5 里表格会不会溢出或错位,改 row_h、col_w 前最好先跑一遍看绘制边界。
class="type">int y1=this.m_header_h+(class="type">int)y; class=class="str">"cmt">//--- Calculate the Y2 coordinate(bottom) depending on the number of lines and their height class="type">int y2=y1+row_h*class="type">int(rows>class="num">0 ? rows : class="num">1); class=class="str">"cmt">//--- Set table coordinates table.SetCoords(x1,y1-this.m_header_h,x2,y2-this.m_header_h); class=class="str">"cmt">//--- Get the class="type">class="kw">color of the table grid lines, either by class="kw">default or passed to the method class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color); class=class="str">"cmt">//--- If the initial X coordinate is greater than class="num">1, draw a table frame class=class="str">"cmt">//--- (in case of the coordinate class="num">1, the table frame is the panel frame) if(x1>class="num">1) this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- In the loop by table rows, for(class="type">int i=class="num">0;i<(class="type">int)rows;i++) { class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row) class="type">int row_y=y1+row_h*i; class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even if(alternating_color && i%class="num">2==class="num">0) { class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45); this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha)); } class=class="str">"cmt">//--- Draw a table grid horizontal line this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Create a new table row object CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object) if(!table.AddRow(row_obj)) class="kw">delete row_obj; class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title row_obj.SetY(row_y-this.m_header_h); } class=class="str">"cmt">//--- In the loop by table columns, for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If the grid line goes beyond the panel, interrupt the loop if(x1==class="num">1 && col_x>=x1+m_canvas.Width()-class="num">2) class="kw">break; class=class="str">"cmt">//--- Draw a vertical line of the table grid
网格自动填充时的单元格坐标落点
在仪表盘里画背景网格,最容易被忽略的是单元格坐标怎么跟表格对象绑定。上面这段逻辑先按 table.RowsTotal() 拿到总行数,再逐行取 CTableRow 指针,为空就跳过——这一步决定了后面循环不会因脏数据崩在半路。 循环体内每生成一个 CTableCell,都先用 row.AddCell(cell) 挂到行上,失败立即 delete 并 continue,避免内存泄漏。随后 cell.SetXY(col_x, row.Y()) 把列横坐标和该行纵坐标写进单元格,这样渲染层才知道每一格画在哪。 DrawGridAutoFill 方法则负责算边界:x1 取 border,x2 是画布宽减 border 再减 1,y1 从表头高度 m_header_h 叠加 border 起算,y2 为画布高减 border 减 1。table.SetCoords(x1,y1,x2,y2) 落定后,网格才会按实际画布尺寸自适应。 实盘用在 MT5 上验证时,把 border 从默认 10 调到 2,能直观看到表格可用区放大、单元格更密;外汇与贵金属图表叠加此类面板属高风险操作,参数乱改可能导致视觉重叠掩盖报价。
this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Get the number of created rows from the table data object class="type">int total=table.RowsTotal(); class=class="str">"cmt">//--- In the loop by table rows for(class="type">int j=class="num">0;j<total;j++) { class=class="str">"cmt">//--- get the next row CTableRow *row=table.GetRow(j); if(row==NULL) class="kw">continue; class=class="str">"cmt">//--- Create a new table cell CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add the created cell to the row class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object) if(!row.AddCell(cell)) { class="kw">delete cell; class="kw">continue; } class=class="str">"cmt">//--- In the created cell object, set its X coordinate and the Y coordinate from the row object cell.SetXY(col_x,row.Y()); } } class=class="str">"cmt">//--- Update the canvas without redrawing the chart this.m_canvas.Update(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draws the background grid with automatic cell sizing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDashboard::DrawGridAutoFill(const class="type">uint table_id,const class="type">uint border,const class="type">uint rows,const class="type">uint columns,const class="type">class="kw">color line_color=clrNONE,class="type">bool alternating_color=true) { class=class="str">"cmt">//--- Get a table object by ID CTableData *table=this.GetTable(table_id); if(table==NULL) { PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); class="kw">return; } class=class="str">"cmt">//--- X1(left) table coordinate class="type">int x1=(class="type">int)border; class=class="str">"cmt">//--- X2(right) table coordinate class="type">int x2=this.m_canvas.Width()-(class="type">int)border-class="num">1; class=class="str">"cmt">//--- Y1(upper) table coordinate class="type">int y1=this.m_header_h+(class="type">int)border; class=class="str">"cmt">//--- Y2(lower) table coordinate class="type">int y2=this.m_canvas.Height()-(class="type">int)border-class="num">1; class=class="str">"cmt">//--- Set table coordinates table.SetCoords(x1,y1,x2,y2); class=class="str">"cmt">//--- Get the class="type">class="kw">color of the table grid lines, either by class="kw">default or passed to the method
「画表格网格时顺手把行对象建好」
这段逻辑在自定义面板里把表格区域按行列数切分,并一边画网格线一边把行对象挂进数据表。先根据 border 参数决定要不要单独画边框:border>0 时用指定颜色画矩形外框,否则直接借用面板边框,省一次绘制调用。 行高用 round(greed_h/rows) 算,列宽用 round(greed_w/columns) 算,都是整数像素,避免亚像素累积错位。循环里每行先算 row_y,若 alternating_color 且行号为偶数,就用 NewColor(clr,45,45,45) 把底色提亮 45 阶再 FillRectangle 铺背景——这是斑马纹的来源。 每行都会 new 一个 CTableRow(i),失败就 PrintFormat 报错并 continue,成功才 AddRow;行对象的 Y 坐标要减去 m_header_h,因为网格坐标是相对面板画的,而行对象内部坐标要扣掉标题栏高度。
class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color); class=class="str">"cmt">//--- If the offset from the edge of the panel is greater than zero, draw a table border, class=class="str">"cmt">//--- otherwise, the panel border is used as the table border if(border>class="num">0) this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Height of the entire table grid class="type">int greed_h=y2-y1; class=class="str">"cmt">//--- Calculate the row height depending on the table height and the number of rows class="type">int row_h=(class="type">int)::round((class="type">class="kw">double)greed_h/(class="type">class="kw">double)rows); class=class="str">"cmt">//--- In the loop based on the number of rows for(class="type">int i=class="num">0;i<(class="type">int)rows;i++) { class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row) class="type">int row_y=y1+row_h*i; class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even if(alternating_color && i%class="num">2==class="num">0) { class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45); this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha)); } class=class="str">"cmt">//--- Draw a table grid horizontal line this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Create a new table row object CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object) if(!table.AddRow(row_obj)) class="kw">delete row_obj; class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title row_obj.SetY(row_y-this.m_header_h); } class=class="str">"cmt">//--- Table grid width class="type">int greed_w=x2-x1; class=class="str">"cmt">//--- Calculate the column width depending on the table width and the number of columns class="type">int col_w=(class="type">int)::round((class="type">class="kw">double)greed_w/(class="type">class="kw">double)columns); class=class="str">"cmt">//--- In the loop by table columns, for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If this is not the very first vertical line, draw it class=class="str">"cmt">//--- (the first vertical line is either the table frame or the panel frame) if(i>class="num">0)
class="type">class="kw">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color); class=class="str">"cmt">//--- If the offset from the edge of the panel is greater than zero, draw a table border, class=class="str">"cmt">//--- otherwise, the panel border is used as the table border if(border>class="num">0) this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Height of the entire table grid class="type">int greed_h=y2-y1; class=class="str">"cmt">//--- Calculate the row height depending on the table height and the number of rows class="type">int row_h=(class="type">int)::round((class="type">class="kw">double)greed_h/(class="type">class="kw">double)rows); class=class="str">"cmt">//--- In the loop based on the number of rows for(class="type">int i=class="num">0;i<(class="type">int)rows;i++) { class=class="str">"cmt">//--- calculate the Y coordinate of the next horizontal grid line(Y coordinate of the next table row) class="type">int row_y=y1+row_h*i; class=class="str">"cmt">//--- if the flag of "alternating" line colors is passed and the line is even if(alternating_color && i%class="num">2==class="num">0) { class=class="str">"cmt">//--- lighten the table background class="type">class="kw">color and draw a background rectangle class="type">class="kw">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45); this.m_canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha)); } class=class="str">"cmt">//--- Draw a table grid horizontal line this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Create a new table row object CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add it to the list of rows of the tabular data object class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object) if(!table.AddRow(row_obj)) class="kw">delete row_obj; class=class="str">"cmt">//--- Set its Y coordinate in the created row object taking into account the offset from the panel title row_obj.SetY(row_y-this.m_header_h); } class=class="str">"cmt">//--- Table grid width class="type">int greed_w=x2-x1; class=class="str">"cmt">//--- Calculate the column width depending on the table width and the number of columns class="type">int col_w=(class="type">int)::round((class="type">class="kw">double)greed_w/(class="type">class="kw">double)columns); class=class="str">"cmt">//--- In the loop by table columns, for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate the X coordinate of the next vertical grid line(X coordinate of the next table row) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If this is not the very first vertical line, draw it class=class="str">"cmt">//--- (the first vertical line is either the table frame or the panel frame) if(i>class="num">0)
◍ 把表格行渲染进画布并落盘像素
在仪表盘绘制流程里,先沿列坐标 col_x 从 y1 到 y2 拉一条带透明度的竖线,颜色由 ColorToARGB(clr, this.m_alpha) 决定,alpha 值直接控制线条通透程度。 随后从 table.RowsTotal() 拿到总行数,循环里逐行取 CTableRow 指针;任一行或单元格 new 出来是 NULL 就跳过并打印函数名与索引,避免空指针把整个面板绘制中断。单元格坐标用 cell.SetXY(col_x, row.Y()) 对齐列与行位置,加不进 row 就立刻 delete 回收。 画完不重绘图表,调 this.m_canvas.Update(false) 只刷新内存画布,MT5 上能少一次主图重绘开销。 FileSaveWorkspace() 负责把工作区像素数组存文件:数组大小为 0 直接返回 false 并提示空数组;FileSave 失败则带 GetLastError() 报错返回 false,否则返回 true。注意 m_filename_ws 是类内指定的文件名,改它就能换存储路径。外汇与贵金属图表加载此类面板存在内存占用波动风险,参数宜在小周期先测。
this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Get the number of created rows from the table data object class="type">int total=table.RowsTotal(); class=class="str">"cmt">//--- In the loop by table rows for(class="type">int j=class="num">0;j<total;j++) { class=class="str">"cmt">//--- get the next row CTableRow *row=table.GetRow(j); if(row==NULL) class="kw">continue; class=class="str">"cmt">//--- Create a new table cell CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add the created cell to the row class=class="str">"cmt">//--- (if adding an object failed, class="kw">delete the created object) if(!row.AddCell(cell)) { class="kw">delete cell; class="kw">continue; } class=class="str">"cmt">//--- In the created cell object, set its X coordinate and the Y coordinate from the row object cell.SetXY(col_x,row.Y()); } } class=class="str">"cmt">//--- Update the canvas without redrawing the chart this.m_canvas.Update(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Save the pixel array of the working space to a file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDashboard::FileSaveWorkspace(class="type">void) { class=class="str">"cmt">//--- If the saved array is empty, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(this.m_array_wpx.Size()==class="num">0) { ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__); class="kw">return false; } class=class="str">"cmt">//--- If the array could not be saved to a file, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!::FileSave(this.m_filename_ws,this.m_array_wpx)) { ::PrintFormat("%s: FileSave &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Save the pixel array of the panel background to a file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDashboard::FileSaveBackground(class="type">void) { class=class="str">"cmt">//--- If the saved array is empty, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
像素数组的落盘与回读容错
做仪表盘类指标时,背景与工作区的像素数组若每次重绘都现算,CPU 占用会随周期切换陡增。把数组存成文件、下次直接加载,是压低开销的直接办法。 存盘前必须确认数组非空,否则 FileSave 写出的文件后续加载会得到空数据,界面可能直接花屏。下面这段保存背景像素的逻辑,先判 Size()==0 再写文件,失败就带错误码返回 false。 if(this.m_array_ppx.Size()==0) { ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__); return false; } //--- If the array could not be saved to a file, report this and return 'false' if(!::FileSave(this.m_filename_bg,this.m_array_ppx)) { ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); return false; } //--- Successful, return 'true' return true; } //+------------------------------------------------------------------+
| // | Upload the array of working space pixels from a file |
|---|
//+------------------------------------------------------------------+ bool CDashboard::FileLoadWorkspace(void) { //--- If failed to upload data from the file into the array, report this and return 'false' if(::FileLoad(this.m_filename_ws,this.m_array_wpx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError()); return false; } //--- Successful, return 'true' return true; } //+------------------------------------------------------------------+
| // | Upload the array of panel background pixels from a file |
|---|
//+------------------------------------------------------------------+ bool CDashboard::FileLoadBackground(void) { if(::FileLoad(this.m_filename_bg,this.m_array_ppx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); return false; } //--- Successful, return 'true' return true; } 回读时用 FileLoad 的返回值与 WRONG_VALUE 比较,而不是看返回真值——MT5 里文件读取失败返回的是特定常量,直接当 bool 用会漏掉错误。两个加载函数(工作区 / 背景)结构一致,只是文件名和数组对象不同。 在 MT5 里跑这套,建议把 m_filename_bg、m_filename_ws 设成带品种名后缀,避免切换图表时不同品种像素互相覆盖。外汇与贵金属品种波动剧烈,这类界面缓存仅用于显示优化,不改变任何交易信号逻辑。
if(this.m_array_ppx.Size()==class="num">0) { ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__); class="kw">return false; } class=class="str">"cmt">//--- If the array could not be saved to a file, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!::FileSave(this.m_filename_bg,this.m_array_ppx)) { ::PrintFormat("%s: FileSave &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Upload the array of working space pixels from a file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDashboard::FileLoadWorkspace(class="type">void) { class=class="str">"cmt">//--- If failed to upload data from the file into the array, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(::FileLoad(this.m_filename_ws,this.m_array_wpx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_ws,::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Upload the array of panel background pixels from a file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDashboard::FileLoadBackground(class="type">void) { if(::FileLoad(this.m_filename_bg,this.m_array_ppx)==WRONG_VALUE) { ::PrintFormat("%s: FileLoad &class="macro">#x27;%s&class="macro">#x27; failed. Error %lu",__FUNCTION__,this.m_filename_bg,::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Successful, class="kw">return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; }
「别急着下结论」
这一节把前面跑过的成交量与比尔·威廉姆斯系列指标在 EA 里的接法收了个口:所有示例源码都能直接拷进你自己的自定义逻辑里用,面板类统一丢到 \MQL5\Include\Dashboard\Dashboard.mqh 即可。随文给的 11 个文件里,Dashboard.mqh 体积 217.85 KB,是其余 10 个测试 EA(单文件 18~38 KB 不等)的公共依赖,少放这一个路径编译就会报缺引用。 外汇与贵金属市场高杠杆、滑点随机,这类接指标代码只解决「信号怎么进 EA」的问题,不替你过滤品种波动风险;真要上模拟盘,先拿 TestWilliamsAlligator.mq5(38.09 KB)验证鳄鱼三线在非常规周期的对齐表现。 作者留了活口:面板库后续文章里迭代过,当前 ZIP 未必是最新。想省事就顺着他文章列表翻最后那篇用到该库的,替换掉旧 mqh 再编译,可能避开 Vitaly 提到的「最小化后切周期露网格」这类界面 bug。