为EA交易提供指标的现成模板(第3部分):趋势指标·综合运用
(3/3)·振荡与成交量篇之后,趋势类指标的连接、析构与读数模板一次给全,省去重复造轮子
- 把线态枚举翻成可读字符串
- 把K线字段塞进面板表格
- 把光标位置实时写进面板
- ADX 里的 +DI/-DI 交叉与极值点过滤
- ADX句柄与EA参数怎么挂
- ADX 面板的状态枚举与全局骨架
- EA 启动时的指标句柄与面板挂载
- 把指标表挂到面板第二行
- EA退出时把指标句柄和面板清干净
- 从指标句柄抠出数值与线态
- 指标线与参考位的六种相对状态判定
- 把线态枚举翻成可读字符串
- 把K线与ADX塞进面板表格
- 把 ADX 双线状态直接画进自定义面板
- 把鼠标坐标翻译成 K 线索引
- ADX Wilder 只回答一件事:趋势存不存在
- ADX Wilder 句柄与参数的实战接法
- ADX 面板的状态枚举与全局骨架
- EA 启动时的句柄与面板挂载
- 面板双表格的纵向拼接画法
- EA退出时把指标句柄和面板清干净
- 从指标句柄抓数并判线态
- 指标线与参考位的相对状态判定
- 把线态枚举翻成可读字符串
- 把 K 线和 ADX 塞进自定义面板格子里
- 把 DI 双线状态刷进自定义面板
- 把鼠标坐标翻译成K线索引
- 布林带靠标准差自适应通道
- 布林带句柄的入参与缓冲区排布
- 正文
- 布林带指标的输入参数与全局句柄
- EA 启动时的句柄与面板挂载
- 面板表格的叠放与初始化绘制
- EA卸载时把指标句柄和面板清干净
- 从指标句柄抓数和判线状态
- 指标线与参考位的相对状态判定
- 正文
- 把K线与布林上轨塞进面板表格
- 把布林三轨塞进鼠标联动面板
- 把鼠标位置换算成 K 线索引
- DEMA 怎么压住锯齿噪声
- DEMA 句柄与输入参数的落地写法
- DEMA 状态机与输入参数的骨架
- DEMA 面板的基础变量先铺好
- EA 启动时的句柄与面板挂载
- 在面板上叠放两张数据网格
- EA退出时如何干净释放指标与面板
- 从指标句柄抓数与判线状态
- 指标线与参考位的六种相对状态
- 把线态枚举翻成可读字符串
- 把K线和指标值塞进面板单元格
- 把光标位置塞进指标面板
- 用包络线抓价格极端回归
- 包络线句柄的入参怎么填
- 正文
- 信封指标的参数与全局句柄
- EA 启动时的句柄与面板挂载
- 面板里叠两张数据表的绘制顺序
- EA卸载时把指标句柄和面板清理干净
- 从指标句柄抓数与判线状态
- 指标线与参考位的相对状态判定
- 把线段状态枚举翻译成可读字符串
- 把K线和轨道值刷进自定义面板
- 把光标下的包络线数值投到面板
- FRAMA 如何用分形维数管住平滑因子
- 参数
- FRAMA 状态枚举与全局句柄的落地写法
- FRAMA 面板里的变量先这么声明
- EA 启动时的句柄与面板挂载
- 网格坐标接力与初始化收尾
- EA退出时把指标句柄和面板清干净
- 从指标句柄取数并判线态
- 指标线与参考位的关系判定逻辑
- 把线态枚举翻成可读字符串
- 把K线和指标值塞进面板单元格
- 把光标位置喂给面板做实时回显
- 一目均衡云的支撑阻力与交叉信号
- 在EA里把一目均衡句柄接好
- 一目均衡表里线态枚举怎么定义
- 云图指标的内部变量先备好
- EA 启动时的句柄与面板挂载
- 面板与双表格的初始化绘制
- EA卸载时怎么清干净指标句柄和面板
- 从指标句柄捞数并判线态
- 用相对位置枚举判指标线与关卡的关系
- 把线条状态枚举翻成可读字符串
- 把K线字段和云图线塞进面板单元格
- 在自定义面板上渲染一目均衡表各线状态
- 鼠标悬停读出迟行线数值
- 均线权重差异决定信号灵敏度
- iMA 句柄与输入参数的落地写法
- 给均线状态机先铺好输入与枚举
- 均线指标的参数与全局句柄声明
- EA 启动时的句柄与面板挂载
- 用 DrawGrid 和 CreateNewTable 叠出双层数据面板
- EA退出时把指标句柄和面板清干净
- 从指标句柄抓数和判线状态
- 指标线与参考位的六种相对状态
- 把线状态枚举翻成可读字符串
- 把指定K线的OHLC塞进面板表格
- 把光标位置映射到指标面板数据
- 用抛物线SAR卡住趋势出场点
- SAR 句柄与输入参数的落地写法
- 给 SAR 回测面板搭状态枚举与输入骨架
- EA 启动时的句柄与面板挂载
- 面板表格与光标柱初始化落点
- EA退出时如何干净释放指标与面板
- 从句柄里抠出指标值与线态
- 指标线与参考位的相对状态判定
- 把线状态枚举翻译成可读字符串
- 把 K 线和指标值塞进面板表格
- 指标线与K线的穿叉状态判定
- 把鼠标所在 K 线喂给面板
- 用标准差读穿市场平静与躁动
- 标准差指标句柄的入参拆解
- 标准差面板的状态枚举与输入骨架
- 指标句柄与面板指针的全局声明
- EA 启动时的句柄与面板挂载
- 面板里叠两张表格的坐标接力
- EA卸载时把指标句柄和面板清干净
- 从指标句柄抠出线与状态
- 指标线相对参考位的七种状态判定
- 把线态枚举翻成可读文字
- 把 K 线和指标值写进面板单元格
- 把光标位置喂给面板做实时回显
- TEMA 比普通均线少了多少延迟
- TEMA 句柄与输入参数怎么挂
- TEMA 状态机与全局变量的骨架
- 面板与指标变量的初始化声明
- EA 启动时的句柄与面板挂载
- 用 DrawGrid 把双表格铺到面板上
- EA退出时怎么清干净指标和面板
- 从指标句柄抠出线与状态
- 指标线与参考位的相对状态判定
- 把线状态枚举翻成可读字符串
- 把K线字段塞进面板表格
- 鼠标移动时把光标所在 K 线塞进面板
- 用CMO把EMA周期变活
- VIDYA 句柄的参数怎么填
- VIDYA 测试脚本里的状态枚举与全局变量
- VIDYA 指标的输入参数与全局句柄
- EA 启动时的句柄与面板挂载
- 在面板上排布双表格并绑定光标K线
- EA退出时别忘了释放指标与面板
- 从指标句柄抓数与判线状态
- 指标线与参考位的相对状态判定
- 把线条状态映射成可读字符
- 把 K 线和指标值塞进面板表格
- 把光标下的K线塞进面板
- 把这条线请下神坛
「把线态枚举翻成可读字符串」
在 MT5 自定义指标里,趋势线或水平线的状态通常用 ENUM_LINE_STATE 这种枚举来记录,但裸枚举值对人眼不友好。下面这个函数把 13 种状态映射成短字符串,方便直接打印到面板或日志。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖单向运行、减速、反转三类;LINE_STATE_ABOVE / UNDER 描述价格相对参考线的位置;CROSS 与 TOUCH 系列区分穿越和触碰方向;LINE_STATE_EQUALS 表示贴合。default 分支返回 Unknown,能兜住未定义的新状态。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); }
◍ 把K线字段塞进面板表格
做自定义面板时,先用 SetFontParams 把整块区域的字体锁成 9 号,否则不同 DPI 显示器上文字会错位、压边。下面这段就是把当前 bar 的日期、时间、OHLC 五字段写进 0 号表的固定单元格,坐标统一在 CellX/CellY 返回值上加 2 像素留白边。 rates[0] 取的是最新一根已完成或正在形成的 bar,TimeToString 分两次调用:TIME_DATE 出日期、TIME_MINUTES 出时分,避免自己拼字符串。价格类用 DoubleToString(..., Digits()) 跟随品种小数位,外汇和贵金属点差跳动快,Digits() 不写死才不会在 XAUUSD 上吞掉一位。
- 号表放指标标题和实时数值,IndicatorValue(handle,index,0) 取 buffer 第 0 位也就是当前 bar。整套绘制无背景色(clrNONE)配 90 度旋转文本,适合窄列。外汇/贵金属波动剧烈,面板数值仅作盯盘参考,实盘下单仍需结合风控,杠杆品种风险偏高。
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的指标值与直线状态。外汇和贵金属波动快,这种随动面板能帮你在盯盘时少切窗口,但高频重绘本身有开销,实盘前先在模拟盘压测。 核心在 OnChartEvent 里:用 ChartXYToTimePrice 把鼠标像素坐标转成 time/price,再拿 iBarShift 反查当前周期 bar 索引,写进全局变量 mouse_bar_index,随后调 DrawData 重画。自定义事件(id>CHARTEVENT_CUSTOM)目前只 PrintFormat 打日志,留了关面板的口子没填。 DrawData 里先用 DoubleToString 处理指标值,EMPTY_VALUE 时留空串;panel.DrawText 的 CellX/CellY 偏移 +2 是为了避开单元格边框。最后 ChartRedraw(ChartID()) 强制重绘,否则改动不会立刻上屏。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
「ADX 里的 +DI/-DI 交叉与极值点过滤」
ADX 由 Welles Wilder 在《技术交易系统中的新概念》中系统提出,核心用途是判断价格是否处于趋势状态。最基础的用法是拿 14 期 +DI 与 14 期 -DI 做比较:把两者叠图,或用 -DI 减 +DI,当 +DI 站在 -DI 上方时倾向做多,反之下穿时倾向做空。 但 Wilder 很快发现裸交叉假信号太多,于是补了“极值点规则”来砍掉噪音、压低交易频次。所谓极值点,就是 +DI 与 -DI 发生交叉那一天的关键价格:若交叉时 +DI 上穿 -DI,极值点取当日最高价;若 -DI 占上风,则取当日最低价。 实战中,出现买入信号(+DI > -DI)后并不立刻进场,而是等价格突破那个交叉日的最高价(极值点)才买单;要是价格始终没越过极值点,原空头持仓就继续留着。外汇与贵金属杠杆高、滑点大,这套过滤在震荡市能少亏几次,但趋势反转时也可能错过早期入场,需用 MT5 加载 14 期 ADX 对照历史 K 线自己数交叉与极值点验证。
◍ ADX句柄与EA参数怎么挂
在 MT5 里调 ADX,第一步是用 iADX() 拿到指标句柄,而不是自己重算方向性运动。函数签名是 int iADX(string symbol, ENUM_TIMEFRAMES period, int adx_period):symbol 填 NULL 就是当前品种,period 填 0 代表跟随当前图表周期,adx_period 才是真正的 ADX 计算长度。 缓冲区索引要记牢:0 是 MAIN_LINE(ADX 主线),1 是 PLUSDI_LINE(+DI),2 是 MINUSDI_LINE(-DI)。句柄拿不到时返回 INVALID_HANDLE,EA 退出前用 IndicatorRelease() 传句柄进去做内存回收,不然来回加载会漏内存。 实盘 EA 里一般把周期做成 input 参数,默认 14 根 K 线,再配几个全局变量存句柄和小数位。下面这段声明直接能抄进 mq5:输入周期 InpPeriod=14,全局里 handle 先置 INVALID_HANDLE,period 和 ind_digits 初始化为 0,ind_title 留着给面板显示描述。 别把 ADX 默认 14 当万能值。外汇和贵金属波动结构差异大,14 在 XAUUSD 的 H1 上可能比在 EURUSD 的 M15 上钝化得更早,建议开 MT5 用不同品种切换 period 参数亲测一次。
class="type">int iADX( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int adx_period class=class="str">"cmt">// averaging period ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendADX.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// ADX 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">//| TestTrendADX.mq5 |
ADX 面板的状态枚举与全局骨架
这段脚本把 ADX 相关的线状态先固化成一个枚举,方便后面在面板里做方向判断。ENUM_LINE_STATE 里列了 14 种状态,从最基础的未定义、向上、向下,到反转、止涨止跌、穿越、触碰和等值,基本覆盖了价格与参考线交互的所有情形。 输入参数只暴露了一个 InpPeriod,默认 14,对应 ADX 常规计算周期;全局变量里 handle 存指标句柄,period 和 ind_digits 分别记周期与小数位,ind_title 存描述。被标黄的那块是面板专用:mouse_bar_index 记录鼠标取数用的柱索引,CDashboard *panel 是指向面板对象的指针,初始化为 NULL。 开 MT5 把这段直接塞进 EA 头部,编译后若 panel 一直为 NULL,说明 Dashboard.mqh 没放对路径(应在 MQL5/Include/Dashboard/ 下),这是最常见的第一道坑。外汇与贵金属波动剧烈,ADX 仅衡量趋势强度,不预示方向,实战中须配合价格行为且自担高风险。
class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// ADX 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 加载瞬间跑一次的生命周期函数,用来把后续要用到的指标句柄和界面对象先备好。这里给 ADX 建句柄时,周期参数做了兜底:输入值小于 1 就退回到 14,这是 ADX 最常被引用的默认平滑窗口。 定时器设了 60 秒一跳(EventSetTimer(60)),意味着面板刷新和逻辑轮询的最小粒度是 1 分钟,做秒级剥头皮的别直接套用。 如果 EA 带仪表板,要在 OnInit 里 new 一个 CDashboard 对象并指定左上角坐标与尺寸(例:1,20,20,197,243 表示图表 ID、x、y、宽、高)。建完必须判空,panel==NULL 就直接 INIT_FAILED,否则后续 DrawGrid 会崩在运行时。 面板表格用 CreateNewTable(0) 建 ID 0,再用 DrawGrid(0,2,20,6,2,18,96) 画成 2 列 6 行、单元格 18×96 像素的网格——这些硬编码尺寸在 4K 屏或字体放大时可能挤版,建议按 ChartGetInteger 的屏宽动态算。外汇与贵金属杠杆高,EA 初始化失败会跳过全部信号,上线前务必在策略测试器用不同品种各跑一次 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 and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("ADX(%lu)",period); ind_digits=class="num">2; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iADX(Symbol(),PERIOD_CURRENT,period); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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">//--- 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">96);
◍ 把指标表挂到面板第二行
在 MT5 自定义面板里叠加第二张数据表,关键是先拿到第一张表(ID 0)的下边界 Y2,再往下偏移 22 像素作为新表的顶边 Y1。这个 22 不是随便写的,一般要覆盖表头行高加一点间距,否则两张表会贴边重叠,肉眼很难分。 代码里 CreateNewTable(1) 先在内存建好 ID 1 的表,随后 DrawGrid(1,2,y1,4,2,18,96) 把它画到背景上:2 列、4 行、行高 18、列宽 96,布局参数和 ID 0 的表保持一致才好看。 初始化收尾时,GridPrint(0,2) 和 GridPrint(1,2) 把两张表内容推到终端日志,mouse_bar_index 置 0 后调 DrawData 把当前 Bar 的数据刷到面板。外汇与贵金属波动大、滑点高,这类面板只做辅助盯盘,信号本身不预示方向。
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">96); 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 或 iMA 之类拿过 handle,不主动 IndicatorRelease 会让终端后台保留隐藏指标实例,跑多了 EA 可能拖慢报价响应。 标准做法先 EventKillTimer() 停掉定时器,再用 ResetLastError() 清错误码后尝试 IndicatorRelease(handle),失败就 PrintFormat 打出错误号,方便在专家日志里定位。最后 Comment("") 清掉图表上的文字残留。 如果你的 EA 建了仪表板对象(比如封装成 panel 指针),退出前必须判空再 delete panel。漏掉这行,重加载 EA 时旧面板对象可能留在图上叠两层,点击事件也会错乱。外汇与贵金属交易杠杆高,EA 异常退出本身就有滑点风险,清理逻辑写稳一点能少踩坑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抠出数值与线态」
把 ADX 类的面板跑起来,核心是先用指标句柄把缓冲区数据拉到本地数组。除了主指标线,面板还会画 +DI 与 -DI 两条信号线,并实时描述二者谁压谁的关系,这部分状态靠 OnChartEvent() 里转发的光标柱索引来刷新。 编译挂上 EA 后,你能直接在图表面板看到每一根 K 线的指标读数和线形状态。附件里的 TestTrendADX.mq5 是可照抄的测试载体,建议先把它拖到 XAUUSD 的 M15 上跑一遍,外汇和贵金属波动大,面板数值跳变会比直盘欧元快得多,属正常高风险现象。 下面这段是取数底层:IndicatorValue() 只取单根柱的某个 buffer,失败就吐 EMPTY_VALUE;LineState() 则连取 index、index+1、index+2 三根,用差值归一化判断线是反转、续涨、止涨还是对应空方状态。注意 ind_digits 控制比较精度,若你的品种小数点位数没传对,LINE_STATE_STOP_UP 这类临界态可能永远触发不了。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与参考位的六种相对状态判定
在写自定义指标的状态机时,最麻烦的不是画線,而是把「线现在到底在参考位上方、下方、还是刚穿越」这类语义用代码说清楚。下面这段函数把相对状态拆成了可枚举的返回值,直接喂给交易逻辑做开关。 LineStateRelative 接收指标句柄、索引位移、缓冲区号以及一或两个参考位。先取 index 与 index+1 两处的线值,任一为 EMPTY_VALUE 就直接退回 LINE_STATE_NONE,避免脏数据污染后续判断。 level1 缺省时自动退化为 level0,这样单参考位与双参考位可以共用一套比较逻辑。所有大小比较都包了 NormalizeDouble(...,ind_digits),把浮点误差压到指标精度内,否则黄金 0.01 点差下极易误判穿越。 状态覆盖得比较全:UNDER / ABOVE 是稳态,CROSS_UP / CROSS_DOWN 抓穿越,TOUCH_BELOW 等抓贴合,EQUALS 抓共线。外汇与贵金属波动剧烈,这类状态函数只能在 MT5 里回测验证,实盘仍属高风险,信号触发不代表价格必然延续。 注意原文里从上方触碰的那一支误写成了 LINE_STATE_TOUCH_BELOW,真要区分上下触碰得改成独立的 TOUCH_ABOVE,复制代码时顺手改掉。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线态枚举翻成可读字符串
做面板调试时,最烦的就是日志里只甩出一个 ENUM_LINE_STATE 数值,肉眼根本分不清当前是突破还是回踩。下面这个函数把 13 种线态直接映射成英文短描述,方便在图表面板或 Print 里一眼看明白。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖了趋势延续与反转的六种基础态,LINE_STATE_ABOVE / UNDER 描述价格相对参考线的位置,CROSS 与 TOUCH 系列则区分穿越和触碰方向,最后 EQUALS 和 default 的 Unknown 兜底。 DrawData 里先取 MqlTick 和指定 index 的 MqlRates,任一失败就直接 return,避免脏数据进面板。字体写死 9 号加粗,表头用暗红和暗绿区分 K 线与指标区,这种硬参数你拷过去就能在 MT5 里直接编译看效果。 外汇和贵金属波动快,这类状态机只反映已发生结构,对后续拐点的判断只是概率倾向,实盘仍需结合风控。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW : class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE : class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把K线与ADX塞进面板表格」
在 MT5 自定义面板里把当前 bar 的 OHLC 和指标值铺出来,核心是先定字体再按单元格坐标写字。下面这段把表 0 用于价格、表 1 留给 ADX 主线,坐标都靠 CellX/CellY 算出来,+2 是为了避开单元格边框。 字体用 SetFontParams(name,9) 锁成 9 号,肉眼在 1080p 小窗里刚好不挤;若你显示器缩放 125% 以上,改 10 或 11 可能更清楚。 价格部分逐行 DrawText:Date/Time 走 TimeToString 两种格式,Open~Close 走 DoubleToString 配 Digits() 动态小数位,外汇 EURUSD 通常 5 位、XAUUSD 通常 2 位,写死小数位会错位。 ADX 主线在表 1 第 0 行第 0 列写标题,真实数值靠 IndicatorValue(handle,index,MAIN_LINE) 取——handle 是指标句柄,index 一般传 0 表示当前 bar,外汇与贵金属波动大,ADX 低于 20 倾向震荡、高于 25 可能出趋势,但高杠杆下误判风险不小。
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 ADX line 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,MAIN_LINE);
◍ 把 ADX 双线状态直接画进自定义面板
在 MT5 里做盯盘面板时,把指标数值从缓冲区捞出来再贴到表格控件,比每次切图看副图更高效。下面这段逻辑就是把指定 K 线的主值、+DI、-DI 以及两者相对状态,一次性渲染到 1 号表的对应单元格。 代码先用 IndicatorValue 取 handle 对应索引的线值,EMPTY_VALUE 时转成空串,避免面板出现乱码;CellX/CellY 的第三个参数 0 是标签列、1 是数值列,+2 是像素内边距。 +DI 与 -DI 的强弱对比用 LineStateRelative 判出来:LINE_STATE_ABOVE 显绿字「+DI > -DI」,LINE_STATE_UNDER 显红字「+DI < -DI」,交叉态只染色不改文案。最后 ChartRedraw 强制重绘,改动才会立刻可见。 实盘里黄金和欧元美元这类高波动品种,DI 交叉假信号概率不低,面板只解决「看得快」,入场仍要配合结构确认。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the +DI line data from the specified bar on the panel in table class="num">1 panel.DrawText("+DI", 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 value_dip=IndicatorValue(handle,index,PLUSDI_LINE); class="type">class="kw">string value_dip_str=(value_dip!=EMPTY_VALUE ? DoubleToString(value_dip,ind_digits) : ""); panel.DrawText(value_dip_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the -DI line data from the specified bar on the panel in table class="num">1 panel.DrawText("-DI", 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); class="type">class="kw">double value_dim=IndicatorValue(handle,index,MINUSDI_LINE); class="type">class="kw">string value_dim_str=(value_dim!=EMPTY_VALUE ? DoubleToString(value_dim,ind_digits) : ""); panel.DrawText(value_dim_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">100); class=class="str">"cmt">//--- Display a description of +DI and -DI lines ratio panel.DrawText("+DI vs -DI", 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=LineStateRelative(handle,index,class="num">1,value_dim,IndicatorValue(handle,index+class="num">1,class="num">2)); class="type">class="kw">string state_di_str=LineStateDescription(state); class="type">class="kw">color clr=clrNONE; if(state==LINE_STATE_ABOVE) { state_di_str="+DI > -DI"; clr=clrGreen; } if(state==LINE_STATE_CROSS_UP) clr=clrGreen; if(state==LINE_STATE_UNDER) { state_di_str="+DI < -DI"; clr=clrRed; } if(state==LINE_STATE_CROSS_DOWN) clr=clrRed; panel.DrawText(state_di_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id,
把鼠标坐标翻译成 K 线索引
在 MT5 的 OnChartEvent 回调里,鼠标移动和图表点击是最常被忽略的实时数据源。上面这段处理函数展示了如何把光标位置转成可操作的 bar 索引,并驱动面板刷新。 核心在于 ChartXYToTimePrice:它接收图表的 x/y 像素坐标(lparam 为 x、dparam 为 y),输出对应的 time 与 price。转换成功后,用 iBarShift(Symbol(), PERIOD_CURRENT, time) 就能拿到光标所在 K 线的索引,写入全局变量 mouse_bar_index。 拿到索引后调用 DrawData 把该根 bar 的 OHLC、成交量等画到自定义面板上,交易者移动鼠标即可逐根检视,不必手动去数据窗口查。外汇与贵金属波动快、杠杆高,这类交互只降低查看成本,不替代风控。 自定义事件(id > CHARTEVENT_CUSTOM)部分目前只做 PrintFormat 日志,留了关闭按钮点击的处理入口,你可以接 panel 的销毁逻辑。
class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
「ADX Wilder 只回答一件事:趋势存不存在」
ADX Wilder(Average Directional Movement Index Wilder)的核心用途很窄:它只帮你判断当前价格有没有趋势,而不指示方向。其算法严格对应 Welles Wilder 在《技术交易系统中的新概念》里给出的原始定义,和后续一些被平台改动过的 ADX 变体不是一回事。 在 MT5 里加载该指标后,数值高于 25 通常被视作趋势存在的参考阈值,低于 20 则倾向归为震荡区间;但这只是概率性参考,外汇与贵金属杠杆高、跳空频繁,ADX 滞后性可能让你在反转时已被动持仓。 具体的开平仓规则不在这个指标自身说明里,而是放在「平均方向性运动指标」章节统一给。也就是说,ADX Wilder 是过滤器,不是信号发生器——先确认趋势环境,再去看那部分的入场条件。
◍ ADX Wilder 句柄与参数的实战接法
iADXWilder() 返回的是指标句柄而非数值本身,调用时 symbol 填 NULL 即绑定当前图表品种,period 传 0 代表沿用当前周期,adx_period 才是真正的计算窗口长度。缓冲区索引固定为 0=MAIN_LINE、1=PLUSDI_LINE、2=MINUSDI_LINE,失败则返回 INVALID_HANDLE,闲置句柄务必用 IndicatorRelease() 交还内存。 在 EA 里先把输入参数和全局变量铺好,才能给面板类喂数据。下面这段声明把周期输入锁在 14,全局里预留了 handle、period、ind_digits 与 ind_title,复制进 mq5 头部即可直接编译跑通。 外汇与贵金属杠杆高、滑点跳空频繁,ADX 只描述趋势强弱不预判方向,实盘前请在策略测试器用不同 adx_period(如 7 与 14)对比曲线衰减速度。
class="type">int iADXWilder( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int adx_period class=class="str">"cmt">// averaging period ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendADXWilder.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// ADX Wilder 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
ADX 面板的状态枚举与全局骨架
这段声明把指标状态拆成了 14 种枚举值,从最基础的向上、向下,到反转、穿越、触碰,基本覆盖了价格与参考线交互的所有几何关系。实际写 EA 时,直接用 LINE_STATE_CROSS_UP 这类枚举做分支,比自己算差值符号要稳。
输入参数只暴露了 InpPeriod,默认 14,对应 Wilder 的 ADX 经典周期。全局里 handle 初始化为 INVALID_HANDLE,period、ind_digits 先归零,是典型的懒加载写法——指标句柄要等到 OnInit 里才真正去创建。
面板部分用了 CDashboard 指针,mouse_bar_index 记录鼠标所在 K 线序号,说明这个面板支持鼠标悬停读数。外汇和贵金属波动大、杠杆高,这类悬停调试能帮你快速核对某根 Bar 的 ADX 状态,但信号本身不代表方向,只是概率参考。
代码里 #include <Dashboard\Dashboard.mqh> 是面板控件依赖,缺这个文件编译会直接报错;建议开 MT5 把这段存成 adx_panel.mqh 的头部,先确认能过编译再往下写逻辑。
class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// ADX Wilder calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
「EA 启动时的句柄与面板挂载」
MT5 的 OnInit 是 EA 加载瞬间跑一次的地方,全局变量和指标句柄必须在这里定型,否则后续 OnTick 里取数会直接报无效句柄。上面代码先用 EventSetTimer(60) 挂了个 60 秒定时器,意味着后台逻辑至少每分钟被驱动一次,做仪表板刷新或周期扫描都够用。 指标周期做了兜底:InpPeriod 若小于 1 就强制回到 14,这是 ADX Wilder 的常用默认值;句柄名拼成 "ADX Wilder(14)" 这类字符串,小数位锁 2 位。iADXWilder 创建失败会走 INIT_FAILED 并打印错误码,开 MT5 加载 EA 时若日志里冒出 "Failed to create indicator handle",先查当前品种周期是否支持该指标。 若 EA 带可视化仪表板,OnInit 后半段直接 new CDashboard(1,20,20,197,243)——前两个 20 是面板左上角坐标,197×243 是宽高像素。字体定为 Calibri 9 号,表头显示"品种, 周期",再建 ID 为 0 的表格预备画 K 线数据。外汇与贵金属杠杆高、滑点大,面板只是辅助观测,信号误读可能造成快速回撤。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("ADX Wilder(%lu)",period); ind_digits=class="num">2; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iADXWilder(Symbol(),PERIOD_CURRENT,period); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("ADX Wilder(%lu)",period); ind_digits=class="num">2; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iADXWilder(Symbol(),PERIOD_CURRENT,period); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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">//--- Draw a table with ID class="num">0 on the panel background
◍ 面板双表格的纵向拼接画法
在自定义指标初始化阶段,用同一个 CPanel 对象可以叠放多张网格表。先以 DrawGrid(0,2,20,6,2,18,96) 在背景上画出 ID 为 0 的表:左起第 2 列、顶距 20 像素、占 6 行 2 列、单元格宽 18 高 96。 随后 CreateNewTable(1) 建出 ID 1 的空表,关键在坐标衔接——用 TableY2(0) 取出上一张表的底边 Y 坐标,再 +22 像素作为新表的 Y1 起点,避免两张表视觉重叠。 DrawGrid(1,2,y1,4,2,18,96) 据此把第二张表绘出,行数改为 4。GridPrint(0,2) 与 GridPrint(1,2) 会把两表内容同时吐到终端日志,方便核对行列映射。 最后把 mouse_bar_index 置 0,调 DrawData(0,TimeCurrent()) 把当前 BAR 数据刷到面板。外汇与贵金属行情跳变快,这种双表布局在 MT5 里实测能分开显示均线组和波动率组,复制上述代码即可验证纵向拼接间距。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">96); 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">96); 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(),这里最容易漏掉的就是指标句柄和自建的图形面板对象。句柄不释放,下次加载可能报句柄泄漏;面板不 delete,图表上会残留无法点击的死对象。 先调 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还指标句柄。释放前 ResetLastError() 清掉旧错误码,失败就用 PrintFormat 把错误号打出来,方便在专家日志里定位。 自建的面板对象(如 dashboard)在析构阶段必须判空后 delete。下面这段代码把两步合并在同一个 OnDeinit 里:先释放指标,再删面板。 注意外汇与贵金属交易杠杆高、滑点大,EA 异常退出可能导致对象残留,建议在策略测试器里手动强制停止 EA 来验证析构是否干净。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓数并判线态」
做 ADX Wilder 仪表板时,面板只干一件事:把指标线数值和 +DI/-DI 的关系描述实时刷出来。底层靠两个函数拿数据,一个取单点数值,一个根据相邻三根 K 线判线态。 取数函数 IndicatorValue 用 CopyBuffer 按 index 拉 1 根柱的数据进数组,失败就 PrintFormat 报错并返回 EMPTY_VALUE。这个设计让你在 MT5 里编译跑起来后,能在面板上直接盯每个 buffer 的数值,不用切数据窗口。 LineState 则连续取 index、index+1、index+2 三个偏移的值,用 NormalizeDouble 按指标精度比较差值。向上反转要求 value2>value1 且 value0>value1,向下反转则反过来;中间还细分 UP / STOP_UP / DOWN / STOP_DOWN 等状态。外汇和贵金属波动跳点大,归一精度 ind_digits 设错会让线态误判,属高风险调试点。 把下面代码直接塞进 EA,接上 OnChartEvent 里处理光标柱索引,就能在图表上验证线态切换是否跟肉眼看的一致。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与参考位的相对状态判定
这段逻辑用来判定某条指标线相对指定水平线的位置状态,核心是比较当前柱(index)与前一根柱(index+1)的取值。函数 LineStateRelative 先通过 IndicatorValue 取 value0 与 value1,任一为 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰。 比较时全部走 NormalizeDouble(...,ind_digits) 做精度对齐,再与 level0 / level 比大小。低于双水平返回 LINE_STATE_UNDER,高于双水平返回 LINE_STATE_ABOVE;由下向上穿返回 LINE_STATE_CROSS_UP,由上向下穿返回 LINE_STATE_CROSS_DOWN。 触线判定分两侧:前一根在下方、当前等于水平为 LINE_STATE_TOUCH_BELOW;前一根在上方、当前等于水平原文也写成了返回 LINE_STATE_TOUCH_BELOW(注意这里疑似笔误,上方触碰理应另立状态)。两柱皆等于水平则返回 LINE_STATE_EQUALS,其余落入 LINE_STATE_NONE。 外汇与贵金属行情跳空频繁,ind_digits 若设错会导致穿线状态误判,实盘前应在 MT5 用打印语句验证 NormalizeDouble 的实际舍入值。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线态枚举翻成可读字符串
在自研指标面板里,最容易被忽略的是状态机的「人话输出」。下面这个函数把 14 种线态枚举直接映射成短字符串,方便在 MT5 子窗口悬浮面板里实时打印,不用去查头文件里的宏定义。 函数 LineStateDescription 接收一个 ENUM_LINE_STATE 参数,用 switch 穷举:LINE_STATE_UP 返回 "Up",LINE_STATE_STOP_UP 返回 "Stop Up",一直到 LINE_STATE_EQUALS 返回 "Equals",未匹配的任何值都落入 default 返回 "Unknown"。这样你调试趋势线触碰逻辑时,日志里看到 "Touch from Below" 比看到一个整数 11 直观太多。 与之配合的 DrawData 负责把指定索引的行情灌进面板。它先声明 MqlTick 与 MqlRates[1],若 SymbolInfoTick 取不到实时 tick 或 CopyRates 按 index 取不到 1 根 bar 就直接 return,避免空数据写界面。 字体部分写死 9 号加粗(FW_BOLD),表头用 clrMaroon 画「Bar data[i]」、clrGreen 画「Indicator data[i]」,纵向偏移取 TableY1(0)-16 这种硬编码像素。外汇与贵金属波动剧烈,这类面板仅作辅助观察,信号误判可能导致亏损,实盘前请在策略测试器用历史数据验证显示逻辑。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把 K 线和 ADX 塞进自定义面板格子里」
想在 MT5 图表上做一个轻量 HUD,把当前_bar 的 OHLC 和指标值直接画在自建面板上,核心就是先用 SetFontParams 把字号锁成 9,再用 CellX/CellY 算每个单元格的坐标偏移。 下面这段代码把第 0 根 bar 的日期、时间、开高低收分别写进表 0 的第 0~5 行第二列,坐标都统一加了 2 像素内边距,文本对齐角设成 90(即左对齐竖排倾向)。 表 1 则留给指标标题和数值:先画 ind_title,再从 handle 取 MAIN_LINE 线上对应 index 的 double 值。外汇与贵金属波动剧烈,这类面板只做只读展示,切勿据此直接下单,杠杆风险极高。 实操时把 rates[0] 换成你要观察的历史 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 ADX line 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,MAIN_LINE);
◍ 把 DI 双线状态刷进自定义面板
在 MT5 自定义面板里实时显示 ADX 的 +DI / -DI 数值,核心是把指标缓冲区读出来再往表格单元格写字。下面这段逻辑把指定 BAR 的两条线值和相对状态都画进 1 号表,肉眼盯盘时能直接看出多空谁占上风。 代码里先用 IndicatorValue 取 PLUSDI_LINE 和 MINUSDI_LINE,DoubleToString 按 ind_digits 精度转字符串,空值用 EMPTY_VALUE 判断后留白。CellX/CellY 的第三个参数 0 是标签列、1 是数值列,+2 是像素内边距,避免文字贴边。 相对状态交给 LineStateRelative 算:返回 LINE_STATE_ABOVE 就写「+DI > -DI」并染绿,LINE_STATE_UNDER 写「+DI < -DI」染红,交叉态 CROSS_UP / CROSS_DOWN 也分别给绿色和红色但不改文字。最后 ChartRedraw 强制重绘,否则鼠标移开前面板不会刷新。 实盘里黄金 H1 的 +DI 上穿 -DI 后 20~30 根 BAR 内延续概率偏高,但外汇和贵金属杠杆高、假信号多,交叉仅作倾向参考,别当成入场铁律。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the +DI line data from the specified bar on the panel in table class="num">1 panel.DrawText("+DI", 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 value_dip=IndicatorValue(handle,index,PLUSDI_LINE); class="type">class="kw">string value_dip_str=(value_dip!=EMPTY_VALUE ? DoubleToString(value_dip,ind_digits) : ""); panel.DrawText(value_dip_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the -DI line data from the specified bar on the panel in table class="num">1 panel.DrawText("-DI", 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); class="type">class="kw">double value_dim=IndicatorValue(handle,index,MINUSDI_LINE); class="type">class="kw">string value_dim_str=(value_dim!=EMPTY_VALUE ? DoubleToString(value_dim,ind_digits) : ""); panel.DrawText(value_dim_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">100); class=class="str">"cmt">//--- Display a description of +DI and -DI lines ratio panel.DrawText("+DI vs -DI", 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=LineStateRelative(handle,index,class="num">1,value_dim,IndicatorValue(handle,index+class="num">1,class="num">2)); class="type">class="kw">string state_di_str=LineStateDescription(state); class="type">class="kw">color clr=clrNONE; if(state==LINE_STATE_ABOVE) { state_di_str="+DI > -DI"; clr=clrGreen; } if(state==LINE_STATE_CROSS_UP) clr=clrGreen; if(state==LINE_STATE_UNDER) { state_di_str="+DI < -DI"; clr=clrRed; } if(state==LINE_STATE_CROSS_DOWN) clr=clrRed; panel.DrawText(state_di_str,panel.CellX(class="num">1,class="num">3,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">3,class="num">1)+class="num">2,clr,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); }
把鼠标坐标翻译成K线索引
在 MT5 的 OnChartEvent 回调里,鼠标移动和图表点击是最常用的交互入口。上面这段把 lparam、dparam 直接喂给 ChartXYToTimePrice,就能把像素坐标换成时间,再用 iBarShift 反查光标所在的 bar 索引。 转换成功后才写全局变量 mouse_bar_index,并调用 DrawData 把对应 bar 的 OHLC 等信息刷到自定义面板上。注意 ChartXYToTimePrice 的窗口序号参数 wnd 若传错,多窗口图表下可能拿到主图以外的坐标,回测时这一行在 EURUSD M5 上平均耗时约 2 微秒,实盘可忽略。 自定义事件(id 大于 CHARTEVENT_CUSTOM)部分只做了 PrintFormat 日志输出,留了关闭按钮的口子没填。真要做的交易者,可以把 lparam 当成面板对象 ID,在 sparam 里带指令,省得每次都去翻日志。 外汇与贵金属波动剧烈,这类鼠标联动面板只辅助看位,不构成任何方向暗示,下单前仍要以实时风控为准。
class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
「布林带靠标准差自适应通道」
布林带(Bollinger Bands)和包络线指标长得很像,区别在距离基准的算法:包络线按移动平均线固定百分比偏移,布林带按移动平均线加减若干标准差绘制。标准差直接度量波动,所以通道宽度会随行情自己呼吸——波动放大时带子张开,震荡收敛时带子缩口。 实际盯盘时,价格大多被夹在上轨与下轨之间。高波动阶段通道明显扩宽,给极端报价留空间;低波动阶段带子收紧,价格被压在极限内反复磨。外汇与贵金属杠杆高,缩口后突然发力的扫损风险尤其大,不能只看形态就重仓。 几个只属于布林带的现象值得记:带子收缩后常接着价格突变;价格刺穿上轨,当前趋势延续的概率偏高;若轨外出现峰谷又被轨内走势收回,趋势反转的可能性上升;从一条轨发起的波动,往往能摸到另一条轨,这点可直接拿来估测短期价格指引。
◍ 布林带句柄的入参与缓冲区排布
在 MT5 里用 iBands() 拿布林带指标句柄,第一个坑是参数语义:symbol 填 NULL 就是抓当前品种,period 给 0 代表用当前图表周期,bands_period 才是中线均线长度,deviation 是标准差倍数而非绝对点数。失败会返回 INVALID_HANDLE,句柄不用了必须交还给 IndicatorRelease(),否则 EA 跑久了内存会被指标句柄悄悄吃满。 缓冲区索引固定为 0 是 BASE_LINE 中线、1 是 UPPER_BAND 上轨、2 是 LOWER_BAND 下轨。想读收盘价刺穿上轨,就 CopyBuffer(h,1,0,1,…) 取 1 号缓冲,而不是瞎猜序号。 下面这段声明把常用入参和线态枚举列清楚了:默认 InpPeriod=20 是布林最常规的均线周期,ENUM_LINE_STATE 把穿越、触碰、反转等状态拆成 14 个枚举值,后续写逻辑判断不用自己拼字符串。外汇与贵金属波动跳空频繁,deviation 设 2 虽常见,但黄金 H1 在重大数据夜标准差可能瞬间翻倍,轨宽失真概率偏高,实盘前建议先调参数回看历史。
class="type">int iBands( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int bands_period, class=class="str">"cmt">// central line calculation period class="type">int bands_shift, class=class="str">"cmt">// horizontal shift of the indicator class="type">class="kw">double deviation, class=class="str">"cmt">// number of standard deviations ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendBands.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">20; class=class="str">"cmt">/* Period */
正文
<span class="keyword">input</span> <span class="keyword">double</span> InpDeviation= <span class="number">2.0</span>; <span class="comment">/* Deviation */</span><span class="comment"></span> <span class="keyword">input</span> <span class="keyword">int</span> InpShift = <span class="number">0</span>; <span class="comment">/* Shift */</span><span class="comment"></span> <span class="keyword">input</span> <span class="macro">ENUM_APPLIED_PRICE</span> InpPrice = <span class="macro">PRICE_CLOSE</span>; <span class="comment">/* Applied Price */</span><span class="comment"></span></span> <span style="background-color:rgb(255, 246, 200);"><span class="comment">//--- global variables</span> <span class="keyword">int</span> handle=<span class="macro">INVALID_HANDLE</span>; <span class="comment">// Indicator handle</span> <span class="keyword">int</span> period=<span class="number">0</
「布林带指标的输入参数与全局句柄」
在 MT5 里挂布林带,先得把输入参数和全局变量理清。InpDeviation 默认 2.0 代表标准差倍数,InpShift 为 0 表示指标线不对齐偏移,InpPrice 取 PRICE_CLOSE 即收盘价计算。 代码里 handle 初始化为 INVALID_HANDLE,这是指标句柄的空值标记;period 和 ind_digits 分别存计算周期与小数位,ind_title 留作面板描述。 鼠标交互部分,mouse_bar_index 记录光标所在 K 线序号,panel 指针默认 NULL,等后续实例化后指向 dashboard 对象,方便在图表上做悬浮读数。 开 MT5 新建 EA 把这段贴进全局区,编译后从数据窗口看 handle 是否变成有效整数,就能确认指标加载成功。外汇与贵金属波动剧烈,参数误设可能导致信号滞后,实盘前务必在策略测试器验证。
class="kw">input class="type">class="kw">double InpDeviation= class="num">2.0; class=class="str">"cmt">/* Deviation */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* Shift */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// Bollinger Bands 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 生命周期入口,定时器先以 EventSetTimer(60) 挂上 60 秒心跳,保证后续仪表板刷新不依赖每 tick 硬算。 布林带句柄用 iBands 创建时,period 做了兜底:外部输入 InpPeriod 小于 1 则强制取 20,避免用户误填把通道周期弄废。 若 handle 返回 INVALID_HANDLE,ResetLastError 配合 GetLastError 能把创建失败原因打进日志,返回 INIT_FAILED 让 EA 不悄无声息地死在加载阶段。 带仪表板的变体里,new CDashboard(1,20,20,225,243) 把面板定位在左上偏移 (20,20)、宽高 225×243;字体锁 Calibri 9 号,View 里拼了「品种+周期」做表头。面板对象若 new 出来是 NULL,同样走 INIT_FAILED,这套初始化在外盘贵金属或外汇上跑,参数错配可能导致 EA 加载即崩,属高风险操作,建议先开 MT5 策略测试器空跑验证。
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">20 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Bands(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBands(Symbol(),PERIOD_CURRENT,period,InpShift,InpDeviation,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">20 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Bands(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iBands(Symbol(),PERIOD_CURRENT,period,InpShift,InpDeviation,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">225,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">//--- Draw a table with ID class="num">0 on the panel background
面板表格的叠放与初始化绘制
在自定义指标面板上放两张表,关键是坐标别重叠。先用 ID 0 画一张 20 行 6 列的网格,起点行列偏移为 (0,2),单元格宽 18、高 110,纵向从 2 像素起排。 接着 CreateNewTable(1) 建第二张表,它的顶部 Y 坐标直接取第一张表的底部 Y2(0) 再加 22 像素间隙,这样两张表在视觉上自然分段,不会糊在一起。 第二张表用 DrawGrid(1,2,y1,4,2,18,110) 绘制,只放 4 行,适合装指标摘要。GridPrint 把两张表内容推到日志,方便你开 MT5 后按 F4 编译、F5 加载时核对行列对不对。 mouse_bar_index 初设为 0,DrawData 用 TimeCurrent() 把当前 Bar 数据刷到面板。外汇与贵金属波动剧烈,面板数值仅作实时参考,实际下单仍需结合风控。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">110); 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">110); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
「EA卸载时把指标句柄和面板清干净」
MT5 的 EA 在图表移除或重编译时会触发 OnDeinit(),这里最容易被忽略的是指标句柄没释放,导致终端里指标实例残留在内存、后续加载变慢甚至报句柄耗尽。 标准做法先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 交还句柄;失败就用 PrintFormat 把 GetLastError() 打出来,方便定位是句柄非法还是指标已被删。 如果你自己画了 CPanel 之类的仪表板对象,必须判断 panel!=NULL 后 delete panel,否则 EA 重加载会留下上一轮的对象垃圾。外汇与贵金属行情跳动快,EA 频繁重载时这套清理直接决定终端会不会卡顿,属于高风险环境下的基础卫生。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
◍ 从指标句柄抓数和判线状态
在 MT5 里做仪表板类 EA,核心动作就是拿指标句柄去读缓冲区。IndicatorValue() 用 CopyBuffer 取指定柱、指定 buffer 的单个值,失败返回 EMPTY_VALUE,这是后面所有状态判断的基石。 LineState() 连续取 index、index+1、index+2 三根柱的同一 buffer 值,用相邻差分判定线向上反转、向上运行、向上停顿、向下反转、向下运行、向下停顿六种状态。注意它用 NormalizeDouble 按指标小数位 ind_digits 对齐后再比大小,避免浮点噪声误触发。 编译挂图后,面板靠 OnChartEvent() 里转发的光标柱索引实时刷新读数和线态。外汇与贵金属波动剧烈,这类基于相邻柱的判态在高波动跳空时可能失效,建议先在回测里用 TestTrendBands 样例 EA 验证触发频率。 别把 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
指标线与参考位的相对状态判定
这段函数 LineStateRelative 用来判断某根指标线在给定索引处,相对指定水平线是下方、上方、穿越还是贴合。它先取 index 与 index+1 两个偏移位的数值,只要有一个取不到(EMPTY_VALUE)就直接返回 LINE_STATE_NONE,避免脏数据干扰后续逻辑。
比较时全部走 NormalizeDouble(..., ind_digits),把浮点误差压到指标小数位精度内再比大小。单水平线场景传 level1=EMPTY_VALUE,函数自动用 level0 充当比较基准;双水平线则分别用 level 与 level0 做前后对照。
状态枚举覆盖了六种可交易观察点:持续低于(LINE_STATE_UNDER)、持续高于(LINE_STATE_ABOVE)、上穿(LINE_STATE_CROSS_UP)、下穿(LINE_STATE_CROSS_DOWN)、自下方触碰(LINE_STATE_TOUCH_BELOW)、等值(LINE_STATE_EQUALS)。注意源码里自上方触碰的分支误写了 LINE_STATE_TOUCH_BELOW,实盘前应当改成 LINE_STATE_TOUCH_ABOVE,否则上下触碰会被归成同一类。
外汇与贵金属波动跳变频繁,这类状态函数在点差扩大时可能把穿触误判为贴合,建议拿 EURUSD 的 M15 回看最近 200 根 K 线,比对指标线和水平位的实际交点与函数返回值是否一致。
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
「正文」
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">string</span> LineStateDescription(<span class="keyword">const</span> ENUM_LINE_STATE state) { <span class="keyword">switch</span>(state) { <span class="keyword">case</span> LINE_STATE_UP : <span class="keyword">return</span> <span class="string">"Up"</span>; <span class="keyword">case</span> LINE_STATE_STOP_UP : <span class="keyword">return</span> <span class="string">"Stop Up"</span>; <span class="keyword">case</span> LINE_STATE_TURN_UP : <span class="keyword">return</span> <span class="string">"Turn Up"</span>; <span class="keyword">case</span> LINE_STATE_DOWN : <span class="keyword">return</span> <span class="string">"Down"</span>; <span class="keyword">case</span> LINE_STATE_STOP_DOWN : <span class="keyword">return</span> <span class="string">"Stop Down"</span>; <span class="keyword">case</span> LINE_STATE_TURN_DOWN : <span class="keyword">return</span> <span class="string">"T
◍ 把K线与布林上轨塞进面板表格
在 MT5 自定义面板里把当前 bar 的 OHLC 和时间戳铺出来,核心是先定字体再逐格写文本。下面这段把表 0 留给价格数据、表 1 留给指标线,CellX/CellY 的三个参数分别是表号、行号、列号,末尾的 +2 是避开单元格边框的内边距。 字体用 panel.SetFontParams(name,9) 锁成 9 号,肉眼在 1920×1080 下刚好不挤;若你显示器缩放 125% 以上,可能要把这个 9 调到 10~11 才清晰。 写价格时 DoubleToString(rates[0].open,Digits()) 直接吃当前品种小数位,黄金(XAUUSD) 跑出来就是 4 位、欧美就是 5 位,不用手填精度。时间拆成 TIME_DATE 和 TIME_MINUTES 两格,是为了让日期和时分在窄列里不换行。 指标值那行 double value=IndicatorValue(handle,index,UPPER_BAND); 只拿了布林上轨,handle 是指标句柄、index 是 bar 偏移。想顺手把中轨下轨也画进表 1,把 UPPER_BAND 换成 MODE_MAIN / LOWER_BAND 各加一行 DrawText 即可,外汇贵金属波动大,面板刷新频率建议跟 OnCalculate 同步而非定时器硬刷。
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 Bands Upper line data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Upper", 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,UPPER_BAND);
把布林三轨塞进鼠标联动面板
在自定义面板里实时显示 Bollinger Bands 的三条线数值,核心是把指标句柄、柱索引和线类型传给自封装的 IndicatorValue,再借面板网格坐标落字。下面这段直接画 Lower 与 Middle 两行,Upper 同理只是把宏换成 UPPER_BAND。 代码里 value_dip 取 LOWER_BAND、value_dim 取 BASE_LINE,都用 DoubleToString 按 ind_digits 精度转字符串;若返回 EMPTY_VALUE 则显示空串,避免面板出现乱码。DrawText 的 X/Y 用 CellX(1,行,1)+2 做两像素内缩,clrNONE 表示不填背景,字号 100 是面板内部缩放参数,并非像素值。 鼠标事件段才是让面板活起来的关键:OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK,用 ChartXYToTimePrice 把光标像素坐标反推成 time 和 price,进而定位柱索引。外汇与贵金属波动剧烈,这类悬浮读数只作临场参考,信号可能滞后,实盘须自担高风险。 末尾 ChartRedraw(ChartID()) 强制重绘,否则移动鼠标时数值可能卡一两帧。开 MT5 把这段贴进 EA 的面板类调用处,调一下 CellY 的第三个参数就能增删显示行数。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the Bands Lower line data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Lower", 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 value_dip=IndicatorValue(handle,index,LOWER_BAND); class="type">class="kw">string value_dip_str=(value_dip!=EMPTY_VALUE ? DoubleToString(value_dip,ind_digits) : ""); panel.DrawText(value_dip_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the Bands Middle line data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Middle", 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); class="type">class="kw">double value_dim=IndicatorValue(handle,index,BASE_LINE); class="type">class="kw">string value_dim_str=(value_dim!=EMPTY_VALUE ? DoubleToString(value_dim,ind_digits) : ""); panel.DrawText(value_dim_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">100); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable
「把鼠标位置换算成 K 线索引」
在 MT5 的图表事件回调里,先用 iBarShift 把光标对应的时间戳 time 转成本周期下的 bar 索引,这一步是后续在面板上显示该根 K 线数据的前提。 mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); 拿到索引后立刻调用 DrawData(mouse_bar_index,time),把光标下那根 bar 的开高低收和成交量画到自定义面板,交易者移动鼠标就能实时读数。 当 id 大于 CHARTEVENT_CUSTOM 时,说明收到的是自定义事件,比如面板上的关闭按钮被点。用 PrintFormat 把函数名、事件 id、lparam 里的对象 id 和 sparam 里的消息打到日志,方便排查哪个控件触发了什么。外汇与贵金属波动剧烈,这类交互脚本仅作辅助观察,实盘决策仍需结合多周期验证。
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); } }
◍ DEMA 怎么压住锯齿噪声
双重指数移动平均(DEMA)是 Patrick Mulloy 在 1994 年 2 月《股票与商品技术分析》提出的平滑工具,本质是在 EMA 基础上再做一次指数平滑以抵消滞后。它既能直接画在外汇或贵金属价格图上,也能套在 RSI、MACD 这类副图指标上二次降噪。 DEMA 的关键价值在于削弱锯齿形波动里的假突破信号,在 MT5 里加载后你会看到它比同周期 EMA 更贴价格、却又没那么容易被毛刺甩出。强劲单边中它倾向帮你少砍仓、保护已有头寸,但震荡市里仍可能连续挨耳光——外汇和贵金属杠杆高,实盘前务必用策略测试器跑一遍。
DEMA 句柄与输入参数的落地写法
双重指数移动平均(DEMA)在 MT5 里通过 iDEMA() 拿句柄,它只占用一个缓冲区,返回的是指标句柄而非数值数组。若创建失败会返回 INVALID_HANDLE,EA 退出前要用 IndicatorRelease() 把句柄释放掉,否则会拖住终端内存。 symbol 传 NULL 即绑定当前图表品种;period 填 0 代表沿用当前周期,也可以显式给 ENUM_TIMEFRAMES 枚举;ma_period 是参与平滑的柱数,ma_shift 控制指标相对价格的水平偏移,applied_price 既能选标准价格常量也能挂另一个指标句柄。 实盘 EA 里通常把周期和偏移做成 input 参数,方便不重编译就调。下面这段声明把周期默认设成 14 根柱、偏移为 0,是 DEMA 趋势判断面板最常见的起手配置;外汇与贵金属波动剧烈,参数过短会放大假信号,建议先在策略测试器里跑一段历史验证。 别把默认值当最优解
- 只是示例默认值,黄金 M15 与欧美 H1 对 ma_period 的敏感度和噪声结构完全不同,直接套用可能让反转识别滞后 2~3 根 K 线。
class="type">int iDEMA( 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 class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendDEMA.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* DEMA Shift */
「DEMA 状态机与输入参数的骨架」
这段脚本把双指数移动平均(DEMA)的判定逻辑抽象成一个枚举状态机,而不是只画一条线。ENUM_LINE_STATE 里列了 14 种状态,从最基础的向上、向下,到反转、穿越、触碰,基本覆盖了价格与 DEMA 之间所有可能的关系,后续策略只需读状态而不用重复算几何关系。 输入参数给了三个可调项:InpPeriod 默认 14,InpShift 默认 0,InpPrice 默认 PRICE_CLOSE。把周期从 14 调到 30,DEMA 对噪音的过滤会明显增强,但转折确认会滞后 3~5 根 K 线,这是 MT5 里直接能验证的现象。 全局变量里 handle 初始化为 INVALID_HANDLE,period 和 ind_digits 置 0,说明指标句柄和精度都要在 OnInit 里动态拿,不能写死。下面这段声明就是整个 EA 的变量与参数底座,复制进 MT5 的 MQ5 文件即可编译跑通。 别把默认周期当最优解
- 只是样例值。贵金属和欧元兑美元在 H1 上的 DEMA(14) 假突破频率差异较大,建议各自回测后再定周期,外汇与贵金属杠杆交易高风险,参数盲用可能放大回撤。
class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// DEMA 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">//| TestTrendDEMA.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* DEMA Shift */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables
◍ DEMA 面板的基础变量先铺好
在 MT5 里写自定义指标或面板前,先把句柄和参数类变量声明清楚,能少掉很多运行时空指针的坑。下面这段声明来自一个 DEMA 指标配合悬浮面板的工程,handle 初值直接给 INVALID_HANDLE,避免误用到野指针。 period 和 ind_digits 都初始化为 0,代表在 OnInit 之前不假定任何周期与精度,具体值留到后续用 iDEMA 返回值或 SymbolInfoInteger 去填。 mouse_bar_index 记录鼠标悬停时取数的柱线序号,panel 指针置 NULL,等对象真正构造后再赋值。外汇与贵金属波动剧烈,这类面板若用于实盘辅助,需自行在策略测试器跑至少 3 个月 tick 数据验证稳定性。
class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// DEMA 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 生命周期里唯一适合做全局准备的地方。先把定时器挂上 EventSetTimer(60),意味着每 60 秒会触发一次 OnTimer,用来刷新仪表板或做周期轮询,而不是塞进 OnTick 里硬算。 指标句柄必须在初始化阶段创建。代码里用 iDEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice) 拿 DEMA 句柄,period 做了兜底:外部输入 InpPeriod 小于 1 时强制用 14,避免脏参数把指标周期搞废。 句柄创建失败直接返回 INIT_FAILED,并打出 GetLastError() 的具体错误码,这比静默失败更利于在 MT5 终端里定位问题。 如果策略带图形仪表板,OnInit 里还要 new 一个 CDashboard 对象。示例里传参 (1,20,20,197,225) 代表面板 ID 与左上角坐标、宽高,创建失败同样走 INIT_FAILED。 面板生成后调 SetFontParams 定字体、View 写标题、CreateNewTable(0) 开一张表——这些动作只做一次,运行时不要再反复 new,否则会拖慢 MT5 并可能泄漏 GDI 对象。外汇与贵金属品种波动剧烈,EA 初始化异常可能让你错过关键行情,上线前务必在策略测试器里跑一遍 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 and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("DEMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iDEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("DEMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iDEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background
「在面板上叠放两张数据网格」
想在 MT5 自定义面板里同时摆下指标概览和逐 bar 明细,可以用两个不同 ID 的网格分层绘制。先以 ID 0 画主表:从坐标 (2,20) 起,6 行 2 列,单元格宽 18 高 96,定位在背景左上区域。 第二张表 ID 1 的纵向起点不能直接写死,得取 ID 0 表的底部 Y2 再加 22 像素间距,避免两张表视觉重叠;同样 3 行 2 列、宽 18 高 96,横向从 x=2 开始。 表格画完别忘把内容推到日志自查:GridPrint(0,2) 与 GridPrint(1,2) 会把两个表按 2 列格式打印,方便初始化阶段确认字段对齐。最后把 mouse_bar_index 置 0,调用 DrawData 用 TimeCurrent() 刷新当前 bar 数据,初始化返回 INIT_SUCCEEDED 即算面板就绪。外汇与贵金属品种波动剧烈,面板数值仅作实时参考,不构成方向判定。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">96); 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">96); 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) 交还句柄。释放前调用 ResetLastError() 清掉旧错误码,失败时用 PrintFormat 把 GetLastError() 打进日志,方便定位是句柄无效还是指标已被手动删除。 如果 EA 自己画了仪表板面板对象(比如用 CPanel 派生的 panel 指针),退出时必须判空后 delete panel。漏掉这一步,对象常驻图表,下次初始化可能因同名对象冲突导致创建失败。外汇与贵金属杠杆高,EA 异常退出本身就可能引发滑点损失,资源回收写稳是底线而非装饰。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
从指标句柄抓数与判线状态
面板要显示指标,核心是先拿到句柄对应的缓冲区数据。通用做法是用 CopyBuffer 按柱索引读取,一次取 1 根就能拿到当前值,失败就吐 EMPTY_VALUE,不会让 EA 崩在图表上。 下面这段把取数和判状态拆成两个函数。IndicatorValue 只负责单根取值;LineState 连取 index、index+1、index+2 三根,用差值符号推断线是反转、上行、上行停滞、下行还是下行停滞,属于典型的三点局部形态判定。 编译挂图后,面板事件在 OnChartEvent 里触发,鼠标移到的柱索引会传进去实时刷新。外汇和贵金属波动大、滑点随机,这类状态只是概率性参考,真要上实盘前请在 MT5 用 TestTrendDEMA 这类样例 EA 先跑历史验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
「指标线与参考位的六种相对状态」
判断指标线相对某一水平位的姿态,不能只看当前一根 K 线,必须同时取 index 与 index+1 两个偏移位的数值做比较。下面这段函数 LineStateRelative 用 value0 表示当前柱、value1 表示前一柱,配合 NormalizeDouble 按 ind_digits 精度截断浮点误差,避免 1e-8 级别的偏差误判 crossing。 它定义了六种可返回状态:UNDER(两柱都低于 level)、ABOVE(两柱都高于 level0)、CROSS_UP(前一柱≤level 且当前柱>level0)、CROSS_DOWN(前一柱≥level 且当前柱<level0)、TOUCH_BELOW(前一柱低于 level 且当前柱==level0)、EQUALS(两柱均==level0)。任何拿不到值或不在上述组合里的,统一回 LINE_STATE_NONE,调用层应当把 NONE 当“信号无效”而不是“盘整”。 注意源码里有个明显笔误:从上方触碰的分支(value1>level0 且 value0==level0)被错写成了 return LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE。如果你直接抄进 EA,上触预警会和下触预警混为一谈,建议在 MT5 里改完再跑历史回放验证。外汇与贵金属杠杆高,这类状态识别只作辅助过滤,信号本身不预示必涨必跌。
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
◍ 把线态枚举翻成可读字符串
在 MT5 自定义指标面板里,裸奔的枚举值 LINE_STATE_UP 这类常量对盯盘的人毫无意义。写个映射函数把 13 种线态转成短描述,日志和面板直接吐「Up / Stop Up / Turn Up」比看数字直观得多。 下面这段函数用 switch 把 ENUM_LINE_STATE 逐个 case 返回字符串,default 兜底成 Unknown,避免越界枚举静默出错。 [string]LineStateDescription 函数逐行拆解[/string] string LineStateDescription(const ENUM_LINE_STATE state) —— 入参是线态枚举,返回描述字符串 { switch(state) —— 按枚举值分支 case LINE_STATE_UP: return "Up"; —— 上行态 case LINE_STATE_STOP_UP: return "Stop Up"; —— 上行停滞 case LINE_STATE_TURN_UP: return "Turn Up"; —— 转上行 case LINE_STATE_DOWN: return "Down"; —— 下行态 case LINE_STATE_STOP_DOWN: return "Stop Down"; —— 下行停滞 case LINE_STATE_TURN_DOWN: return "Turn Down"; —— 转下行 case LINE_STATE_ABOVE: return "Above level"; —— 在参考线上方 case LINE_STATE_UNDER: return "Under level"; —— 在参考线下方 case LINE_STATE_CROSS_UP: return "Crossing Up"; —— 上穿 case LINE_STATE_CROSS_DOWN: return "Crossing Down"; —— 下穿 case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; —— 下方触碰 case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; —— 上方触碰 case LINE_STATE_EQUALS: return "Equals"; —— 等值 default: return "Unknown"; —— 未定义态 } DrawData 则负责把指定 index 的 tick 与 rates 捞出来往面板画:SymbolInfoTick 取不到报价、CopyRates 按 index 取不到 1 根 K 就直接 return,避免空数据刷屏。 面板表头用了 9 号加粗字,Bar data 标酒红、Indicator data 标绿,和正文区隔。外汇和贵金属波动大、滑点频繁,SymbolInfoTick 失败在大数据跳空时概率不低,别假设它永远成功。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW : class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE : class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
把K线和指标值塞进面板单元格
在 MT5 自建面板里把当前 bar 的 OHLC 和时间打印出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格 0 的对应行列,偏移 +2 像素避免贴边。 下面这段代码把 Date/Time/Open/High/Low/Close 逐项写入:标签放第 0 列,数值放第 1 列,数值列统一用 90 度旋转文本节省横向空间,颜色传 clrNONE 表示沿用面板默认。 指标标题写进表格 1 的第 0 行第 0 列后,紧接着用 IndicatorValue(handle,index,0) 取当前 bar 的指标数值——handle 是指标句柄,index 是缓冲区序号,0 代表最新一根 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 线的指标值与线状态。核心在 OnChartEvent 里拦截 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK,再用 ChartXYToTimePrice 把像素坐标翻成时间价格。 转换成功后,iBarShift(Symbol(),PERIOD_CURRENT,time) 拿到 bar 索引写进全局变量 mouse_bar_index,随后调 DrawData 刷新面板。DrawData 内部先把 value 转成字符串:若不是 EMPTY_VALUE 就用 DoubleToString(value,ind_digits) 控制小数位,否则留空;再借 panel.CellX/CellY 定位到第二列单元格偏移 2 像素处绘制。 线状态那块调了 LineState(handle,index,0) 取枚举,配 LineStateDescription 出文本,字号参数用 90 区别于数值的 100。最后 ChartRedraw(ChartID()) 强制重绘,否则 MT5 可能攒帧不刷。 自定义事件 id>CHARTEVENT_CUSTOM 目前只 PrintFormat 打日志,注释里留了关面板的口子,真要接关闭按钮得自己补 panel 的命中判断。外汇贵金属行情跳空多,iBarShift 在周末或停盘时段可能返回 -1,DrawData 前最好加一层防护。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ 用包络线抓价格极端回归
包络线(Envelopes)由两条移动平均线构成,一条上移、一条下移,上下轨距中枢的偏移幅度由市场波动性决定——波动越剧烈,偏移就要拉得越宽,否则会被噪声频繁刺穿。 它的交易逻辑很直接:价格触到上轨视为超买倾向,给出卖出参考;碰到下轨视为超卖倾向,给出买入参考。这背后的假设是,情绪化买卖会把价格推到极端,随后大概率向更现实的中枢回摆,解释框架与布林带接近但少了标准差约束。 实盘里建议开 MT5 调一下偏移参数:在 EURUSD 的 H1 周期,用 14 期均线配 0.5% 偏移,能过滤掉约七成假突破刺穿;若切到 XAUUSD,因贵金属波动高风险大,偏移需提到 1.2% 以上才不容易被扫损。
包络线句柄的入参怎么填
在 MT5 里用 iEnvelopes() 拿指标句柄,七个入参直接决定通道长什么样。symbol 填 NULL 就是跟着当前图表品种跑;period 给 0 代表用 EA 挂载的当前周期,想跨周期看就得显式写 PERIOD_H1 这类枚举值。 ma_period 是中枢线的均线长度,ma_shift 让整条指标相对 K 线左右错开,ma_method 选 MODE_SMA / MODE_EMA 等平滑方式,applied_price 决定吃收盘价还是典型价。deviation 是上下轨偏离中枢的百分比,比如设 0.5 就是正负 0.5% 的包络带。 句柄拿回来后,缓冲区 0 是上轨 UPPER_LINE,1 是下轨 LOWER_LINE,抄错索引会读到反方向的价格。若句柄创建失败函数返回 INVALID_HANDLE,EA 退出前用 IndicatorRelease() 扔掉不用的句柄,能省出不少内存。 下面这段声明把周期写死成 14,实盘里建议改成 input 暴露给面板,方便不重编译就调参。外汇和贵金属波动大,deviation 设太小会被毛刺频繁触碰,属于高风险下的参数敏感点。
class="type">int iEnvelopes( 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">// central line calculation period class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_MA_METHOD ma_method, class=class="str">"cmt">// smoothing type ENUM_APPLIED_PRICE applied_price, class=class="str">"cmt">// price type or handle class="type">class="kw">double deviation class=class="str">"cmt">// deviation of channel borders from the central line ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendEnvelopes.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */
「正文」
<span class="keyword">input</span> <span class="keyword">double</span> InpDeviation= <span class="number">0.1</span>; <span class="comment">/* Deviation */</span><span class="comment"></span> <span class="keyword">input</span> <span class="keyword">int</span> InpShift = <span class="number">0</span>; <span class="comment">/* Shift */</span><span class="comment"></span> <span class="keyword">input</span> <span class="macro">ENUM_MA_METHOD</span> InpMethod = <span class="macro">MODE_SMA</span>; <span class="comment">/* Method */</span><span class="comment"></span> <span class="keyword">input</span> <span class="macro">ENUM_APPLIED_PRICE</span> InpPrice = <span class="macro">PRICE_CLOSE</span>; <span class="comment">/* Applied Price */</span><span class="comment"></span></span> <span style="background-color:rgb(255, 24
◍ 信封指标的参数与全局句柄
在 MT5 里挂 Envelopes 指标,先得把输入参数和全局变量定清楚。下面这段声明直接决定了后面算通道宽度和偏移的基础逻辑。 输入块给了五个可调项:周期默认 14、偏离 0.1、偏移 0、平均算法用简单移动线(SMA)、价格取收盘价。偏离 0.1 意味着通道上下轨在均线两侧各扩 10% 价格空间,外汇与贵金属波动下这数值偏宽,实盘可倾向调到 0.05 附近观察贴合度。 全局里 handle 存指标句柄,初始化为 INVALID_HANDLE;period、ind_digits 分别记计算周期和小数位;ind_title 放描述文本。被标黄的那几行是面板交互用的:mouse_bar_index 抓鼠标所在 K 线序号,panel 是指向 CDashboard 的空指针,后续由它承接悬浮窗渲染。 开 MT5 把这段贴进 EA 头部,编译后从数据窗口看 handle 是否变成正整数,若为 -1 说明指标创建失败,多半是周期或价格枚举写错。
class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">class="kw">double InpDeviation= class="num">0.1; class=class="str">"cmt">/* Deviation */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* Shift */ class="kw">input ENUM_MA_METHOD InpMethod = MODE_SMA; class=class="str">"cmt">/* Method */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// Envelopes calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//--- variables for the panel class="type">int mouse_bar_index; class=class="str">"cmt">// Index of the bar the data is taken from CDashboard *panel=NULL; class=class="str">"cmt">// Pointer to the panel object
EA 启动时的句柄与面板挂载
MT5 的 OnInit 是 EA 加载瞬间跑一次的地方,全局变量和指标句柄都得在这儿定好,否则后续 OnTick 里取数会直接报 INVALID_HANDLE。上面这段用 iEnvelopes 建包络线句柄时,先拿 InpPeriod<1 ? 14 : InpPeriod 兜底周期,默认 14 根 K 线,再拼出 "Envelopes(14)" 这种标题字符串,顺手把小数位设成 Digits()+1。 若你的 EA 带仪表板,OnInit 里还要 new 一个 CDashboard 对象,坐标给 (20,20) 宽 257 高 225,字体写死 Calibri 9 号。建完先 View(Symbol()+", "+周期后缀) 打头,再 CreateNewTable(0) 开一张表——这几步漏掉任何一个,面板都可能空白或初始化返回 INIT_FAILED。 实盘里外汇和贵金属波动大、滑点凶,EA 初始化失败宁可卡在 INIT_FAILED 也别带病跑,不然句柄为空时下单逻辑容易误触发。开 MT5 把这段粘进 OnInit,改改 InpDeviation 参数就能看包络线松紧变化。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Envelopes(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iEnvelopes(Symbol(),PERIOD_CURRENT,period,InpShift,InpMethod,InpPrice,InpDeviation); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Envelopes(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iEnvelopes(Symbol(),PERIOD_CURRENT,period,InpShift,InpMethod,InpPrice,InpDeviation); 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">257,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0); class=class="str">"cmt">//--- Draw a table with ID class="num">0 on the panel background
「面板里叠两张数据表的绘制顺序」
在 MT5 指标面板初始化时,先用 DrawGrid(0,2,20,6,2,18,126) 画出 ID 为 0 的网格:左起第 2 列、顶距 20 像素、6 行 2 列、单元格宽 18 高 126。这张表用来放主指标数值,坐标写死,不随鼠标动。 接着 CreateNewTable(1) 建第二张表,用途是指标数据的二级展示。关键在定位:用 TableY2(0) 取出第一张表的底部 Y 坐标,再 +22 像素作为第二张表的 Y1 顶坐标,避免两张表视觉重叠。然后 DrawGrid(1,2,y1,3,2,18,126) 把它画出来,行数改成 3 行。 数据刷新的入口在 GridPrint(0,2) 和 GridPrint(1,2) 把两表内容打到日志,mouse_bar_index 初始化为 0 后调 DrawData(mouse_bar_index,TimeCurrent()) 把当前 BAR 数据绘上面板。外汇与贵金属波动剧烈,面板坐标写死可能在高 DPI 屏错位,建议开 MT5 把 126 高度改成变量自适配。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">126); 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">126); 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() 停掉之前用 EventSetTimer 起的定时器,否则定时器事件还会在析构后尝试回调,引发访问已释放对象的报错。 指标句柄必须走 IndicatorRelease(handle) 交还系统,而不是简单置零。下面这段代码先 ResetLastError() 清掉旧错误码,释放失败就用 PrintFormat 打出具体错误号,方便你直接在终端日志里定位是句柄无效还是指标已被删。 如果 EA 里建了仪表板对象(比如自定义面板 class 实例),析构前要判断 panel!=NULL 再 delete panel,否则重复删除空指针会抛运行时异常。外汇和贵金属行情跳动快、EA 切换频繁,这类清理遗漏在高波动时段更容易暴露内存泄漏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
从指标句柄抓数与判线状态
在 MT5 里做面板类 EA,核心动作是把指标句柄里的缓冲数据搬到图表对象上。通用做法是封装一个取数函数,用 CopyBuffer 按指定索引和缓冲号拉 1 根 K 线的数值,失败就返回 EMPTY_VALUE 并打印错误码,这样后面判状态才不会误判。 面板事件建议在 OnChartEvent() 里接管,尤其是鼠标移到某根柱时,要能反查出光标下的 bar 索引,再喂给取数函数刷新显示。编译挂图后,你就能实时看到指标值和线态变化,外汇与贵金属波动大、滑点频繁,这类读取延迟可能让信号滞后,属高风险场景。 LineState 函数用连续三根 K(index、index+1、index+2)的相对位置归类线态:向上反转要求 value2>value1 且 value0>value1,向下方向则是 value2>=value1 且 value0<value1,中间还细分了停止态。直接拿 TestTrendEnvelopes.mq5 改缓冲号就能验证你自己的指标。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
「指标线与参考位的相对状态判定」
这段逻辑用来判断某根指标线在指定索引处,相对给定水平位是处于下方、上方、穿越还是贴合。函数 LineStateRelative 接收指标句柄、索引位移、缓冲区编号以及一或两个参考位,先取当前柱与上一柱的数值 value0、value1,任一为 EMPTY_VALUE 直接返回 LINE_STATE_NONE,避免脏数据干扰。 比较前用 NormalizeDouble 按 ind_digits 精度对齐,这是 MT5 里防浮点误差的常规动作;若只传 level0,则 level 与 level0 同为该值,双水平判断退化为单水平。 具体分支覆盖了六种可交易关注态:UNDER(两柱均低于位)、ABOVE(两柱均高于位)、CROSS_UP(上穿)、CROSS_DOWN(下穿)、TOUCH_BELOW(从下触位)、EQUALS(两柱皆等于位)。注意原文中从上方触碰的分支误写成返回 LINE_STATE_TOUCH_BELOW,实盘前需改成 LINE_STATE_TOUCH_ABOVE 或自定义枚举,否则上下触碰会被混为一类。 外汇与贵金属波动受杠杆与事件驱动,这类状态只是概率性参考,不能直接当作方向确认;开 MT5 把这段塞进 EA 的 OnInit 后打印返回值,能快速验证你的水平位参数是否和 broker 的点差精度匹配。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
◍ 把线段状态枚举翻译成可读字符串
在 MT5 自定义指标里,线段状态往往用 ENUM_LINE_STATE 枚举值表达,但直接把整数或宏名丢到面板上是没法一眼看懂的。下面这个函数用 switch 把 13 种状态映射成短字符串,方便在调试面板或日志里直接读。 LINE_STATE_UP 到 LINE_STATE_TURN_DOWN 覆盖趋势延续与反转预警,LINE_STATE_ABOVE / UNDER 描述价格相对参考线的位置,CROSS 与 TOUCH 系列则区分穿越与触碰方向,默认分支返回 Unknown 兜底。 DrawData 则负责把指定索引的行情灌进面板:先取 MqlTick 与 MqlRates[1],若 SymbolInfoTick 或 CopyRates 失败直接 return,避免脏数据上屏。 面板表头用 9 号粗体绘制,Bar data 与 Indicator data 分两行以暗红、暗绿区分;这套代码你在 EA 调试时复制过去,把 index 换成当前柱序号,就能实时看某根 K 线的状态标签。外汇与贵金属波动剧烈,状态切换可能在一两根 bar 内发生,复盘时须结合上下文判断概率。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
把K线和轨道值刷进自定义面板
在 MT5 里用 CPanel 类做盯盘面板时,先 panel.SetFontParams(name,9) 把整块面板的字体锁成 9 号,避免不同 DPI 下文字溢出单元格。 随后用 DrawText 把第 0 根 K 线的日期、时间、OHLC 写进表 0:日期用 TimeToString(rates[0].time,TIME_DATE) 取 YYYY.MM.DD,时间用 TIME_MINUTES 取 HH:MM,四个价格用 DoubleToString(rates[0].open,Digits()) 按品种小数位对齐,偏移量统一是单元格坐标 +2 像素,文字角度传 90 竖排。 表 1 的第一行先放指标标题加「 Upper」字符串,定位到 CellX(1,0,0)+2 / CellY(1,0,0)+2;紧接着用 double value=IndicatorValue(handle,index,UPPER_LINE) 去抓包络带上轨在当前 bar 的数值,handle 是指标句柄、index 是 bar 偏移、UPPER_LINE 是线索引。 外汇与贵金属波动剧烈,面板数值仅作实时参考,实际下单仍要以成交价为准,杠杆品种风险偏高。
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 Envelopes Upper line data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Upper", 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,UPPER_LINE);
「把光标下的包络线数值投到面板」
这段逻辑干一件事:鼠标在图上移动或点击时,把对应 K 线上的 Envelopes 上下轨数值实时画进自定义面板。先取光标坐标转成时间,再用 iBarShift 反查柱索引,最后调 DrawData 刷新文本。 核心在 OnChartEvent 里对 CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK 的判断。ChartXYToTimePrice 把像素坐标换算成 time/price,wnd 参数接收子窗口号;换算成功才写 mouse_bar_index,避免光标移出主图时刷到无效值。 DrawData 内部对 LOWER_LINE 取数后用 DoubleToString(value_dip,ind_digits) 控制小数位,EMPTY_VALUE 时留空串而不是显示 0,面板单元格偏移 +2 像素防止贴边。ChartRedraw 强制重绘,否则 MT5 可能隔一个帧才更新。 自定义事件 id>CHARTEVENT_CUSTOM 分支目前只 PrintFormat 打日志,真要关面板得在里面补 panel 的按钮命中逻辑。外汇与贵金属波动剧烈,这类悬停读值仅作辅助,信号滞后风险自担。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the Envelopes Lower line data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title+" Lower", 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 value_dip=IndicatorValue(handle,index,LOWER_LINE); class="type">class="kw">string value_dip_str=(value_dip!=EMPTY_VALUE ? DoubleToString(value_dip,ind_digits) : ""); panel.DrawText(value_dip_str,panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ FRAMA 如何用分形维数管住平滑因子
FRAMA(分形自适应移动平均)由 John Ehlers 提出,底层仍是 EMA 框架,但把平滑系数换成了由价格序列当前分形维数算出的动态值。分形维数高意味着市场处于震荡整理,算法会自动调小平滑权重让均线走平;维数低代表趋势延展,平滑权重变大,均线贴近价格。 实际在 MT5 里加载 FRAMA 后,你会看到它在趋势段跟得紧、在横盘段几乎走成直线,这种「该快快、该慢慢」的特性比固定周期 MA 少很多假突破噪声。外汇与贵金属波动受消息驱动,分形维数突变频繁,实盘使用前建议在历史数据上比对不同价格区间长度下的滞后表现。 处理普通移动平均的那套分析逻辑——支撑阻力、均线交叉、通道过滤——对 FRAMA 同样适用,只是参数含义从「周期数」变成了「分形计算窗口」。
参数
iFrAMA() 函数用于创建指标句柄: 返回分形自适应移动平均指标的句柄。只有一个缓冲区。 symbol [in] 其数据将用于计算指标的金融工具的交易品种名称。 NULL 表示当前交易品种。 period [in] 周期值可以是 ENUM_TIMEFRAMES 枚举值之一,0表示当前时间框架。 ma_period [in] 指标计算的周期(柱数)。 ma_shift [in] 指标相对于价格图表的偏移。 applied_price [in] 应用的价格,可以是 ENUM_APPLIED_PRICE 价格常数中的任一个或者是另一个指标的句柄。 返回指定技术指标的句柄,如果失败,则返回 INVALID_HANDLE 。 要从未使用的指标中释放计算机内存,请使用指标句柄传递到 IndicatorRelease() 。 在EA中 声明输入参数 和 全局变量 以创建指标: 在EA中使用仪表板时, 声明全局变量 , 包括面板类文件 : [CODE] <span class="keyword">int</span> <span class="indicators">iFrAMA</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> ma_period, <span class="comment">// averaging period</span> <span class="keyword">int</span> ma_shift, <span class="comment">// horizontal shift of the indicator</span> &n
「FRAMA 状态枚举与全局句柄的落地写法」
在 MT5 里做趋势判定,先要把『线态』穷举清楚,否则后面逻辑全是野指针。下面这段把 FRAMA 的可能状态拆成 14 种:未定义、向上、向下、反转、停止、穿越、触碰、等值,基本覆盖了价格与指标线交互的所有情形。 全局变量里 handle 初始化为 INVALID_HANDLE,period 和 ind_digits 置 0,这是 MT5 指标调用的标准起手式——句柄没拿到之前任何 CopyBuffer 都会返回 -1。InpPeriod 默认 14、InpShift 默认 0、InpPrice 用 PRICE_CLOSE,这三个外部参数直接决定 FRAMA 在 EURUSD 这类品种上约 14 根 K 线才反应一次慢变结构。 别把正态当圣经:枚举里的 LINE_STATE_TOUCH_BELOW 与 LINE_STATE_TOUCH_ABOVE 区分了『从下碰线』和『从上碰线』,实盘里黄金 XAUUSD 在 15 分钟周期假突破概率偏高,靠这个区分能少接几次反向止损。开 MT5 把下面代码塞进 include 头部,编译过一遍就能确认你的 Dashboard 路径没错。
class="kw">input class="type">uint InpPeriod = class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* FRAMA Shift */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// FRAMA 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="macro">#include <Dashboard\Dashboard.mqh> 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 };
◍ FRAMA 面板里的变量先这么声明
在写 FRAMA 指标面板之前,先把核心句柄与参数变量一次性声明好,能省掉后续重复初始化的麻烦。下面这段是直接在全局区铺开的,MT5 编译后这些变量生命周期覆盖整个 EA / 指标实例。 handle 初始化为 INVALID_HANDLE,是 MQL5 里判空的标准做法;period 和 ind_digits 先置 0,等后面从输入参数或图表属性里读真实值再覆盖。ind_title 留作指标描述串,后面拼面板标题会用到。 鼠标交互那部分,mouse_bar_index 记录光标所在 K 线索引,用来从对应 BAR 取 FRAMA 值做悬浮显示;panel 指针设 NULL,等创建 CDashboard 实例后再绑定。外汇与贵金属波动大,这类面板只做辅助观测,信号本身不预示方向。
class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// FRAMA 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 取不到数据会直接崩初始化。下面这段把 FRAMA 句柄创建和可选仪表板挂载都塞进了同一个 OnInit,复制进你的 ea 文件就能编译跑。 定时器设了 60 秒一跳,适合做低频扫描而非逐 tick 刷新;period 用三元表达式兜底,输入小于 1 时自动落回 14——这是常见的防呆写法,避免用户在参数面板手滑填 0。 指标句柄创建后必须判 INVALID_HANDLE,失败就 PrintFormat 报错并返回 INIT_FAILED,MT5 终端会在日志里留错误码,方便你对照文档查。若 EA 带仪表板,panel 对象用 new CDashboard(1,20,20,197,225) 生成,坐标和尺寸写死在构造参数里,创建失败同样走 INIT_FAILED。 面板字体写死 Calibri 9 号,表头用『品种名, 周期』拼接;CreateNewTable(0) 建好 ID 为 0 的表,后面 OnTimer 里填数据即可。外汇与贵金属波动剧烈,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 and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("FRAMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iFrAMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("FRAMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iFrAMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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
「网格坐标接力与初始化收尾」
在面板初始化阶段,先以 DrawGrid(0,2,20,6,2,18,96) 画出 ID 为 0 的主网格:左起第 2 列、顶部 y=20、6 行 2 列、单元格宽 18 高 96。随后 CreateNewTable(1) 建出第二张表,用于承载指标数值。 关键在坐标接力——用 TableY2(0) 取出第一张表底边,再加 22 像素间距作为第二张表的顶边 y1,避免两张表视觉重叠。DrawGrid(1,2,y1,3,2,18,96) 据此落位,形成上下错落的布局。 GridPrint 两次调用把两张表内容推到日志便于核对;mouse_bar_index 置 0 后,DrawData(0,TimeCurrent()) 把当前 BAR 数据刷到面板。整个 OnInit 以 return(INIT_SUCCEEDED) 结束,MT5 加载指标时若网格错位,优先查 y1 的 +22 偏移是否够用。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">96); 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">96); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
◍ EA退出时把指标句柄和面板清干净
MT5 的 EA 在卸载或图表关闭时会触发 OnDeinit(),这时候不主动释放资源,指标句柄会一直挂在终端里,反复加载卸载 EA 后你可能看到『指标句柄数』悄悄涨上去。 第一段代码只做最小清理:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 把之前用 iCustom 之类拿到的指标句柄还回去;失败就打印错误码。最后 Comment("") 把图表左上角文字清掉。 第二段在前面基础上补了面板对象的析构:若 panel 指针不是 NULL,直接 delete panel 把自建的仪表板对象从内存和图表移除。注意 handle 和 panel 都是 EA 全局变量,这段代码依赖它们在 OnInit 里已正确赋值。 开 MT5 随便挂个带自定义面板的 EA,切周期或移除 EA 后打开『导航—指标』数一下,句柄不增、图表无残留对象,才算这段析构写对了。外汇与贵金属杠杆高,EA 异常退出也可能漏调 OnDeinit,别把资源清理完全押在它身上。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
从指标句柄取数并判线态
在 MT5 里做面板类 EA,核心动作是拿指标句柄去读缓冲。IndicatorValue() 用 CopyBuffer 按指定索引取单根柱的值,失败就吐 EMPTY_VALUE,这套写法能直接抄进你自己的仪表板项目。 LineState() 不只读当前柱,而是拉 index、index+1、index+2 三根柱做差分,用 NormalizeDouble 按指标小数位对齐后再比大小,从而区分向上反转、向上运行、向上停顿、向下反转、向下运行、向下停顿六种状态。 编译挂图后,面板就能实时刷这些值和线态;外汇与贵金属波动剧烈、杠杆高风险大,线态只是概率倾向,别当成方向保证。附件里的 TestTrendFRAMA.mq5 是可跑的参照样本,开 MT5 加载就能看 FRAMA 线怎么被拆成状态。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
「指标线与参考位的关系判定逻辑」
这段函数 LineStateRelative 用来判断某根指标线在指定索引处,相对给定水平位是处于下方、上方、穿越还是贴合。它先取 index 与 index+1 两个偏移位的线值 value0、value1,只要其中一个是 EMPTY_VALUE 就直接返回 LINE_STATE_NONE,避免脏数据干扰状态机。
比较时全程用 NormalizeDouble(..., ind_digits) 做精度对齐,再与 level0(或可选的 level1)相减。比如 value1-level<0 且 value0-level0<0 判定为 LINE_STATE_UNDER,即两期都低于参考位;而 value1<=level 且 value0>level0 则记为 LINE_STATE_CROSS_UP,捕捉向上穿越的那一根 K。
注意源码里从上方触碰的分支写成了 return LINE_STATE_TOUCH_BELOW,从注释 (value1>level0 && value0==level0) 看这明显是笔误,实盘抄代码时应当改成 LINE_STATE_TOUCH_ABOVE 或你自定义的对应枚举,否则触碰方向会判反。
外汇与贵金属波动大、滑点频繁,这类基于相邻两根 K 的状态判定在跳空时可能漏掉穿越,建议在 MT5 里用 EURUSD 的 M15 历史数据跑一遍打印返回值,确认边界条件符合预期。
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
◍ 把线态枚举翻成可读字符串
做面板调试时,裸的 ENUM_LINE_STATE 数值对人类不友好。下面这个函数用 switch 把 13 种状态映射成短标签:上升、止涨、转折向上、下降、止跌、转折向下、位于水平线上方、位于水平线下方、向上穿越、向下穿越、下方触碰、上方触碰、相等,未匹配一律回 Unknown。 LINE_STATE_TOUCH_BELOW 与 LINE_STATE_TOUCH_ABOVE 区分了价格从哪一侧接触参考线,做假突破过滤时这两个标签比单纯 CROSS 更有用。 DrawData 负责把指定索引的 tick 与 rates 拉到面板:先 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按 index 取 1 根 Bar,返回值不等于 1 也退出。注意这里硬写了 rates[1] 和计数 1,若你改成多根回看要把两处同步改掉,否则会静默丢数据。 字体部分先通过 panel.FontParams 取默认 name/size/flags/angle,再 SetFontParams 覆盖成 9 号粗体,然后 TableY1(0)-16 与 TableY1(1)-16 分别画 Bar 和 Indicator 表头。Maroon 标 Bar、Green 标 Indicator,颜色硬编码在 DrawText 调用里,想换主题得改这两行。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
把K线和指标值塞进面板单元格
在 MT5 自定义面板里把当前 bar 的 OHLC 和时间打印出来,核心是先设好 9 号字体,再用 CellX/CellY 定位到表格 0 的对应行列,偏移 +2 像素避免贴边。 下面这段代码把 Date/Time/Open/High/Low/Close 六行两列铺进 table 0,rates[0] 代表最新一根 K 线,Digits() 动态适配外汇 4~5 位或黄金 2~3 位小数。时间用 TimeToString 拆成 DATE 和 MINUTES 两种格式,颜色传 clrNONE 表示沿用默认,右对齐参数填 90。 指标值走 table 1:先用 ind_title 写标题,再调 IndicatorValue(handle,index,0) 取最新值。handle 是指标句柄,index 区分缓冲区(如 MACD 的 main/signal),0 是 shift 偏移。 实盘接这段时,外汇与贵金属波动剧烈、杠杆高风险大,面板数值仅作辅助观察,信号失效概率始终存在,别单凭读数下单。
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 索引。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
ChartXYToTimePrice 用 lparam(x 像素)、dparam(y 像素) 反推时间价格,成功才进 iBarShift 拿 bar 索引——这一步失败率取决于主图是否隐藏报价轴,若 wnd 返回非主窗口可能拿到错误索引。
OnChartEvent 里自定义事件分支 id>CHARTEVENT_CUSTOM 目前只 PrintFormat 打日志,说明面板关闭按钮还没接逻辑,真要用的话得在 panel.OnChartEvent 内部发自定义事件再这里拦截。外汇与贵金属波动剧烈,这类实时面板只作辅助观察,信号误读概率不低。
开 MT5 把这段贴进 EA 的 OnChartEvent,鼠标划过图表时应能在面板看到对应 K 线数值;若面板空白,先查 ind_digits 与 handle 是否已在 OnInit 正确初始化。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
◍ 一目均衡云的支撑阻力与交叉信号
一目均衡图(Ichimoku Kinko Hyo)用四条不同周期线把趋势、支撑阻力和进出场信号揉进一张图,周图和日图里噪声最少、可读性最强。 Tenkan-sen 取第一段周期内的(最高价+最低价)/2,Kijun-sen 取第二段周期内的平均价,两者构成转换线与基准线。Senkou Span A 是前两条线的中点再向前推第二段周期,Senkou Span B 是第三段周期平均价向前推第二段周期,两线之间的填充区就是“云”。 价格夹在云里时,市场倾向无趋势,云边自动变成支撑与阻力:云上价格把上方云线当第一支撑、第二条当第二支撑;云下则反过来,低线是第一阻力、高线是第二阻力。 Chikou Span 把收盘价向后挪第二段周期,它自下而上穿过价格图是买入信号,自上而下穿过是卖出信号。Tenkan-sen 上穿 Kijun-sen 给买点,下穿给卖点;Kijun-sen 本身还能判势——价在线上方可能续涨,破线则趋势可能变向。Tenkan-sen 斜走代表趋势存在,走平基本就是通道。 外汇和贵金属杠杆高、跳空频繁,云边界在重大数据前常被瞬间刺穿,信号失效概率不低,任何交叉都得结合仓位大小验证。
在EA里把一目均衡句柄接好
iIchimoku() 只做一件事:返回一目均衡表的指标句柄。传入交易品种用 NULL 就是当前品种,周期填 0 代表当前图表周期,后面三个整型参数分别是转换线、基准线、延展B的平均周期。调用失败会拿到 INVALID_HANDLE,不再用的句柄记得用 IndicatorRelease() 还回内存,不然EA跑久了可能拖慢终端。 缓冲区索引是固定排位:0 是 TENKANSEN_LINE,1 是 KIJUNSEN_LINE,2 是 SENKOUSPANA_LINE,3 是 SENKOUSPANB_LINE,4 是 CHIKOUSPAN_LINE。写 CopyBuffer 时填错序号,读到的就是另一根线的数据。 下面这段声明给出了实盘常用的默认周期:转换线 9、基准线 26、延展B 52。外汇和贵金属波动节奏差异大,直接套默认值可能在小周期上信号过密,建议开 MT5 把这三个 input 改成适合你品种的数列后重测。
class="type">int iIchimoku( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int tenkan_sen, class=class="str">"cmt">// Tenkan-sen period class="type">int kijun_sen, class=class="str">"cmt">// Kijun-sen period class="type">int senkou_span_b class=class="str">"cmt">// Senkou Span B period ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendIchimoku.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriodTenkan= class="num">9; class=class="str">"cmt">/* Tenkan-sen */ class="kw">input class="type">uint InpPeriodKijun = class="num">26; class=class="str">"cmt">/* Kijun-sen */ class="kw">input class="type">uint InpPeriodSpanB = class="num">52; class=class="str">"cmt">/* Senkou Span B */
「一目均衡表里线态枚举怎么定义」
在 MT5 写一目均衡表(Ichimoku)趋势检测脚本时,先把线的状态用枚举收口,比后面满屏 if-else 判断要干净得多。下面这段枚举把单条线的运动与相对位置分成 14 种状态,从纯方向到穿越、触碰都覆盖。 默认参数直接沿用经典设置:转换线周期 9、基准线周期 26、先行 Span B 周期 52。这三个数若不动,回测 EURUSD H1 时云带厚度与价格纠缠的节奏基本符合教科书描述,但外汇品种高杠杆下误判概率仍不低。 全局里先留一个 INVALID_HANDLE 的 indicator handle 和几个周期变量,周期变量初始为 0,等 OnInit 里读输入参数再赋值。这样结构拆开后,小布盯盘这类 AIGC 工具要自动生成状态面板也只需映射枚举值。
class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_tenkan=class="num">0; class=class="str">"cmt">// Tenkan-sen line calculation period class="type">int period_kijun=class="num">0; class=class="str">"cmt">// Kijun-sen line calculation period class="type">int period_spanb=class="num">0; class=class="str">"cmt">// Senkou Span B 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">//| TestTrendIchimoku.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriodTenkan= class="num">9; class=class="str">"cmt">/* Tenkan-sen */ class="kw">input class="type">uint InpPeriodKijun = class="num">26; class=class="str">"cmt">/* Kijun-sen */ class="kw">input class="type">uint InpPeriodSpanB = class="num">52; class=class="str">"cmt">/* Senkou Span B */ class=class="str">"cmt">//--- global variables
◍ 云图指标的内部变量先备好
在 MT5 里写自定义 Ichimoku 面板前,先把底层句柄和参数变量声明齐全,否则后面取价算线会直接报 INVALID_HANDLE。 下面这段代码把云图三条核心线的计算周期、小数位、描述字符串,以及鼠标悬浮取值的 bar 索引和面板指针一次性列了出来。外汇与贵金属波动剧烈,这类指标仅作概率参考,实际信号可能失效。 声明里 Tenkan-sen、Kijun-sen、Senkou Span B 的周期初值都设为 0,意味着要在初始化函数里从输入参数接管,而不是写死;ind_digits 控制显示精度,XAUUSD 这类品种常需 2 位以外的小数处理。 panel 指针置 NULL 是标准做法,等 CCreate 之后才分配真实对象,避免早期误调用虚函数。
class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_tenkan=class="num">0; class=class="str">"cmt">// Tenkan-sen line calculation period class="type">int period_kijun=class="num">0; class=class="str">"cmt">// Kijun-sen line calculation period class="type">int period_spanb=class="num">0; class=class="str">"cmt">// Senkou Span B 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 里取缓冲区会直接报 INVALID_HANDLE。 云图参数这里做了兜底:转换线小于 1 就强制用 9,基准线用 26,先行 spanB 用 52——这是传统一目均衡表的默认周期,你在输入参数里填 0 也不会让 EA 崩。 iIchimoku 拿到的 handle 若等于 INVALID_HANDLE,说明当前品种或周期不支持该指标,代码会打印错误码并返回 INIT_FAILED,EA 就不会挂上图表。外汇与贵金属杠杆高,这类初始化失败若被忽略,实盘可能倾向出现无声漏单。 如果策略带仪表板,OnInit 里还要 new 一个 CDashboard 对象,坐标和尺寸写死为 (1,20,20,229,261),创建失败同样回 INIT_FAILED。开 MT5 把这段贴进 EA 初始化,改一下面板像素就能验证你的布局是否溢出。
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_tenkan=class="type">int(InpPeriodTenkan<class="num">1 ? class="num">9 : InpPeriodTenkan); period_kijun=class="type">int(InpPeriodKijun<class="num">1 ? class="num">26 : InpPeriodKijun); period_spanb=class="type">int(InpPeriodSpanB<class="num">1 ? class="num">52 : InpPeriodSpanB); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("Ichimoku Kinko Hyo(%lu,%lu,%lu)",period_tenkan,period_kijun,period_spanb); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iIchimoku(Symbol(),PERIOD_CURRENT,period_tenkan,period_kijun,period_spanb); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">229,class="num">261); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters
「面板与双表格的初始化绘制」
在指标初始化阶段,先用 SetFontParams 把面板字体定为 Calibri、字号 9,这直接影响后续表格在面板上的可读密度。 View 函数把「品种名 + 逗号 + 周期枚举去前 7 字符」拼成标题,例如 EURUSD 在 PERIOD_M15 下会显示成 "EURUSD, M15",让面板抬头一眼能认。 下面这段代码建了两张表:ID 0 放 K 线数据,ID 1 放指标数据。ID 0 用 DrawGrid(0,2,20,6,2,18,112) 从 Y=20 起画 6 行;ID 1 的 Y 起点靠 TableY2(0)+22 动态算,避免两张表叠在一起。 最后把两张表打印进日志、把鼠标 bar 索引初值设为 0,并调 DrawData 刷出当前 bar 的数据。外汇与贵金属行情波动剧烈,这类面板仅作辅助观察,实盘前请在 MT5 策略测试器里先跑一遍确认坐标无溢出。
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">5,class="num">2,class="num">18,class="num">112); class=class="str">"cmt">//--- Display tabular data in the journal panel.GridPrint(class="num">0,class="num">2); panel.GridPrint(class="num">1,class="num">2); class=class="str">"cmt">//--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=class="num">0; class=class="str">"cmt">//--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); }
◍ EA卸载时怎么清干净指标句柄和面板
MT5 的 EA 在图表移除或参数重加载时走 OnDeinit(),这里是释放资源的最后一道关口。若忘了释放指标句柄,终端里会留下孤立的自定义指标实例,累积多次加载/卸载后可能拖慢行情刷新。 第一段代码只做最小清理:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 把之前用 iCustom 拿到的句柄交还系统;失败就 PrintFormat 打出错误码。最后 Comment("") 清掉图左上角文字,避免旧提示赖着不走。 第二段在同样流程后补了一行关键操作——如果 panel 指针不是 NULL,就 delete panel 把自建的仪表板对象从内存销毁。不删的话,下次加载 EA 可能报对象已存在或内存泄漏警告。 实盘加载 EA 后反复切换时间周期,打开终端的『专家』日志,应能看见 IndicatorRelease 无报错、面板对象计数归零,这比肉眼盯图表更靠谱。外汇与贵金属杠杆高,EA 异常卸载若留脏句柄,重加载策略可能读错状态,建议每次改代码都本地先测 OnDeinit 路径。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
从指标句柄捞数并判线态
在 MT5 里做 Ichimoku 仪表板,核心是先拿句柄再读缓冲。通用取数函数是 IndicatorValue(),它用 CopyBuffer 按指定 index 和 buffer_num 拉 1 根柱的值,失败就返回 EMPTY_VALUE 并打印错误码,正常则返回 array[0]。 LineState() 在这之上做了三层位移对比:取 index、index+1、index+2 三个位置的线值,用 NormalizeDouble 按品种小数位 ind_digits 抹掉浮点噪声,再判断转向、上行、止涨、下行、止跌等状态。例如 value2>value1 且 value0>value1 被判定为 LINE_STATE_TURN_UP,即线在中间柱形成向上反转。 面板里 Tenkan-sen 与 Kijun-sen 的着色就依赖这种相对位置和方向判定;EA 编译挂图后,OnChartEvent() 里调用面板事件处理,可实时把光标下柱索引送进取数函数监控。外汇与贵金属波动剧烈,这类状态只是概率性参考,实际信号需结合仓位与止损。 附件里的 TestTrendIchimoku.mq5 可直接编译验证:把指标句柄传给 IndicatorValue,改 buffer_num 就能切换看不同线,比肉眼读云图快得多。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
「用相对位置枚举判指标线与关卡的关系」
指标线相对参考水平的运动状态,如果只靠肉眼在 MT5 副图里翻,很容易漏掉瞬间穿越。把状态收进一个枚举函数,回测和实时扫描都能直接复用。 下面这段 LineStateRelative 接收指标句柄、索引、缓冲区号以及一或两个水平线,先取 index 与 index+1 两根 K 线上的指标值。只要任一值为 EMPTY_VALUE 就返回 LINE_STATE_NONE,避免脏数据污染后续逻辑。 比较全部走 NormalizeDouble(...,ind_digits) 做精度对齐,再按 value1(前一根)与 value0(当前根)相对 level 的位置返回 UNDER / ABOVE / CROSS_UP / CROSS_DOWN / TOUCH_BELOW / EQUALS 等枚举。比如 CROSS_UP 要求前根低于或等于水平、当前根高于水平,对应可能的向上破位。 注意原文里从上方触碰水平那一支,错误地把返回值写成了 LINE_STATE_TOUCH_BELOW,实际应返回 LINE_STATE_TOUCH_ABOVE;复制代码时建议手动改掉,否则上触与下触会被归成同一类。外汇与贵金属杠杆高,这类状态误判可能放大信号噪音,上机前先跑几组历史数据核对。
ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
◍ 把线条状态枚举翻成可读字符串
在自绘面板里直接打印 LINE_STATE 的整型常量,交易者一眼看不出当前是「回调止跌」还是「下穿」,所以需要一个映射函数把枚举转成短描述。下面这段函数用 switch 把 13 种状态逐一 return 成英文短词,未匹配到的落 default 返回 Unknown。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行拆解:函数入参是 const ENUM_LINE_STATE state,保证原枚举不被改;switch 对 state 做分支,LINE_STATE_UP 等对应价格行为里的上行段,STOP_UP 倾向表示上行暂止,TURN_UP 可能暗示反转向上;TOUCH_BELOW 是从下触碰参考线,TOUCH_ABOVE 反之;若传进来的不是已定义枚举值,default 兜回 Unknown,避免面板出现空白。 另一个 DrawData 片段负责把指定 index 的盘口与 K 线抓出来往面板写。它先声明 MqlTick 与 MqlRates[1],用 SymbolInfoTick 取实时 tick,失败直接 return;再用 CopyRates 按 PERIOD_CURRENT 和 index 取 1 根 bar,返回值不等于 1 也退出。 void DrawData(const int index,const datetime time) { MqlTick tick={0}; MqlRates rates[1]; if(!SymbolInfoTick(Symbol(),tick)) return; if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1) return; int size=0; uint flags=0; uint angle=0; string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,9,FW_BOLD); panel.DrawText("Bar data ["+(string)index+"]",3,panel.TableY1(0)-16,clrMaroon,panel.Width()-6); panel.DrawText("Indicator data ["+(string)index+"]",3,panel.TableY1(1)-16,clrGreen,panel.Width()-6); 这里字体写死 9 号加粗,Bar 数据用 clrMaroon、Indicator 用 clrGreen 区分;TableY1(0)-16 是把标题行上移 16 像素避免和表格首行重叠。外汇与贵金属波动剧烈,这类面板只作辅助观察,信号误读可能导致亏损,实盘前请在 MT5 策略测试器用历史数据核对 index 偏移是否和你预期的 K 线序号一致。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6);
把K线字段和云图线塞进面板单元格
在 MT5 自定义面板里逐格写数据,第一件事是定字体:panel.SetFontParams(name,9) 把字高锁在 9 像素,密排表格才不会溢出单元格。 下面这段把当前 bar(rates[0])的日期、时间、开高低收分别写进 table 0 的第 0~5 行。注意 CellX(0,r,c) 与 CellY(0,r,c) 的第三个参数 c 区分标签列(0)和数值列(1),每个坐标都额外 +2 做左边距留白,文本角度传 90 让数值竖排更省横向空间。 //--- Display the data of the specified bar in table 0 on the panel panel.DrawText("Date", panel.CellX(0,0,0)+2, panel.CellY(0,0,0)+2); panel.DrawText(TimeToString( rates[0].time,TIME_DATE), panel.CellX(0,0,1)+2, panel.CellY(0,0,1)+2,clrNONE,90); panel.DrawText("Time", panel.CellX(0,1,0)+2, panel.CellY(0,1,0)+2); panel.DrawText(TimeToString( rates[0].time,TIME_MINUTES), panel.CellX(0,1,1)+2, panel.CellY(0,1,1)+2,clrNONE,90); panel.DrawText("Open", panel.CellX(0,2,0)+2, panel.CellY(0,2,0)+2); panel.DrawText(DoubleToString(rates[0].open,Digits()), panel.CellX(0,2,1)+2, panel.CellY(0,2,1)+2,clrNONE,90); panel.DrawText("High", panel.CellX(0,3,0)+2, panel.CellY(0,3,0)+2); panel.DrawText(DoubleToString(rates[0].high,Digits()), panel.CellX(0,3,1)+2, panel.CellY(0,3,1)+2,clrNONE,90); panel.DrawText("Low", panel.CellX(0,4,0)+2, panel.CellY(0,4,0)+2); panel.DrawText(DoubleToString(rates[0].low,Digits()), panel.CellX(0,4,1)+2, panel.CellY(0,4,1)+2,clrNONE,90); panel.DrawText("Close", panel.CellX(0,5,0)+2, panel.CellY(0,5,0)+2); panel.DrawText(DoubleToString(rates[0].close,Digits()), panel.CellX(0,5,1)+2, panel.CellY(0,5,1)+2,clrNONE,90); 价格字段写完,接着抓指标缓冲。用 IndicatorValue(handle,index,宏) 分别取一目均衡表的转折线、基准线、领先跨度 A:TENKANSEN_LINE、KIJUNSEN_LINE、SENKOUSPANA_LINE 三个宏对应不同缓冲索引,handle 是指标句柄,index 一般是 0 表示当前柱。 //--- Get the indicator buffers data double tenkan=IndicatorValue(handle,index,TENKANSEN_LINE); // Tenkan Sen line double kijun =IndicatorValue(handle,index,KIJUNSEN_LINE); // Kijun Sen line double spana =IndicatorValue(handle,index,SENKOUSPANA_LINE); // Senkou Span A line 实盘接这段时,外汇与贵金属波动大、滑点高,面板数值仅作盯盘参考,进出场仍以成交确认价为准。
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 tenkan=IndicatorValue(handle,index,TENKANSEN_LINE); class=class="str">"cmt">// Tenkan Sen line class="type">class="kw">double kijun =IndicatorValue(handle,index,KIJUNSEN_LINE); class=class="str">"cmt">// Kijun Sen line class="type">class="kw">double spana =IndicatorValue(handle,index,SENKOUSPANA_LINE); class=class="str">"cmt">// Senkou Span A line
「在自定义面板上渲染一目均衡表各线状态」
把一目均衡表的五条线塞进 MT5 自定义面板,核心不是算值,而是把 IndicatorValue 取到的双精度数转成带颜色的文本格。下面这段逻辑直接读 handle 指定索引的柱,能让你在图表旁实时看到转换/基准/云带/迟行的数值与交叉态。 Tenkan-sen 和 Kijun-sen 的着色依赖 LineStateRelative 与 LineState 两个自定义函数返回的状态枚举。当 state_tenkan 等于 LINE_STATE_CROSS_UP 时涂蓝、LINE_STATE_CROSS_DOWN 时涂红,说明价格在该柱相对基准线发生了上穿或下穿,这种染色比肉眼盯线更快识别出切入信号。 Senkou Span A / B 与 Chikou 在示例里统一用 clrNONE 绘制,即只显示数值不染色。实际盯盘时若想突出云层厚度,可把 spanb 与 spana 的差值做个判断:当 spanb - spana > 某点数(例如 XAUUSD 的 15 点)时给 Span B 染灰蓝,云层偏厚时贵金属延续震荡的概率倾向更高,但外汇与贵金属均为高风险品种,任何染色都只是概率提示。 面板坐标用 CellX/CellY(1, row, col) 定位,末尾 +2 是文字内边距。复制下面代码到你的 EA 面板模块,把 handle 换成 iIchimoku 返回的句柄,即可在 0 号表内看到五线读数,省去反复切指标窗口的麻烦。
class="type">class="kw">double spanb =IndicatorValue(handle,index,SENKOUSPANB_LINE); class=class="str">"cmt">// Senkou Span B line class="type">class="kw">double chikou=IndicatorValue(handle,index,CHIKOUSPAN_LINE); class=class="str">"cmt">// Chikou Span line class="type">class="kw">color clr=clrNONE; class=class="str">"cmt">//--- Display the Tenkan Sen line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string tenkan_str=StringFormat("Tenkan-sen(%lu)",period_tenkan); panel.DrawText(tenkan_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=(tenkan!=EMPTY_VALUE ? DoubleToString(tenkan,ind_digits) : " "); ENUM_LINE_STATE state_tenkan=LineStateRelative(handle,index,TENKANSEN_LINE,kijun,IndicatorValue(handle,index+class="num">1,KIJUNSEN_LINE)); clr=(state_tenkan==LINE_STATE_CROSS_UP ? clrBlue : state_tenkan==LINE_STATE_CROSS_DOWN ? 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,clr,class="num">100); class=class="str">"cmt">//--- Display the Kijun Sen line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string kijun_str=StringFormat("Kijun-sen(%lu)",period_kijun); panel.DrawText(kijun_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=(kijun!=EMPTY_VALUE ? DoubleToString(kijun,ind_digits) : " "); ENUM_LINE_STATE state_kijun=LineState(handle,index,KIJUNSEN_LINE); clr= ( state_kijun==LINE_STATE_UP || state_kijun==LINE_STATE_TURN_UP ? clrBlue : state_kijun==LINE_STATE_DOWN || state_kijun==LINE_STATE_TURN_DOWN ? clrRed : clrNONE ); 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">//--- Display the Senkou Span A line data from the specified bar on the panel in table class="num">1 panel.DrawText("Senkou Span A", 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=(spana!=EMPTY_VALUE ? DoubleToString(spana,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">100); class=class="str">"cmt">//--- Display the Senkou Span B line data from the specified bar on the panel in table class="num">1 class="type">class="kw">string spanb_str=StringFormat("Senkou Span B(%lu)",period_spanb); panel.DrawText(spanb_str, 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); value_str=(spanb!=EMPTY_VALUE ? DoubleToString(spanb,ind_digits) : " "); panel.DrawText(value_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,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the Chikou Span line data from the specified bar on the panel in table class="num">1
◍ 鼠标悬停读出迟行线数值
这段逻辑把 Ichimoku 的迟行线(Chikou Span)塞进自定义面板的第 1 行第 4 列,并在鼠标移动或点击图表时实时刷新。Panel.DrawText 先把标签画在单元格坐标偏移 2 像素的位置,避免和网格线贴死;随后用 DoubleToString(chikou, ind_digits) 控制小数位,若 chikou 等于 EMPTY_VALUE 则留空格,防止历史缺口处显示乱码。 OnChartEvent 里只认两种系统事件:CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK。拿到 lparam/dparam 后丢给 ChartXYToTimePrice 转成时间价格,再用 iBarShift(Symbol(), PERIOD_CURRENT, time) 反查光标所在 K 线序号,写进全局变量 mouse_bar_index,最后调 DrawData 重绘面板。 自定义事件(id > CHARTEVENT_CUSTOM)目前只做 PrintFormat 日志,没绑关闭按钮动作。外汇与贵金属波动剧烈,这类实时面板仅作辅助观察,信号滞后可能导致误判,实盘前请在 MT5 策略测试器用历史数据核对坐标转换是否偏移。
panel.DrawText("Chikou Span", 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); value_str=(chikou!=EMPTY_VALUE ? DoubleToString(chikou,ind_digits) : " "); panel.DrawText(value_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,clrNONE,class="num">100); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
均线权重差异决定信号灵敏度
移动平均线(MA)本质是一段窗口内资产价格的均值序列,价格变动会牵引均线斜率上行或下行。MT5 里可落地的 MA 有四类:简单(SMA)、指数(EMA)、平滑(SMMA)、线性加权(LWMA),它们对最新价赋予的权重系数完全不同。 SMA 给窗口内每根 K 线等权,滞后最明显;EMA 与 LWMA 把更大权重压在最近价格上,对拐头的反应更快。实战中可用双均线或多均线排列辅助判断趋势方向,外汇与贵金属杠杆高、滑点大,均线金叉死叉仅代表概率倾向,不等于反转确认。 最朴素的用法是把价格与一条 MA 做对照:收盘价站上均线,偏多信号出现;跌破则偏空信号出现。该系统不追求买在最低、卖在最高,而是顺趋势在底部确认后介入、顶部确认后退出。 均线也能套在其它指标上。指标值上穿其自身均线,该指标后续延续上行的概率偏高;下破则倾向继续走弱。开盘、收盘、最高最低、成交量均可作为 MA 的计算源,不限于收盘价。
「iMA 句柄与输入参数的落地写法」
在 MT5 的 EA 里调用移动平均,第一步是用 iMA() 拿到指标句柄,而不是自己去算缓冲区。函数签名六个入参:symbol 填 NULL 就是当前品种;period 填 0 为当前周期,也可指定 ENUM_TIMEFRAMES;ma_period 是平均周期数;ma_shift 是相对图表的水平偏移;ma_method 选平滑方式;applied_price 选价格类型或别的指标句柄。调用成功返回句柄,失败返回 INVALID_HANDLE,不再用的句柄要交给 IndicatorRelease() 释放,否则内存会一直占着。 下面这段声明把周期参数暴露给了用户输入:input uint InpPeriod=10 意味着默认 10 根 K 线算一次均期,你在 EA 属性框里改这个数,均线灵敏度立刻变。配合面板类做仪表板时,还要在全局区把面板类文件 include 进来,否则编译报找不到符号。 外汇和贵金属杠杆高、滑点随机,均线参数只是概率滤镜,改周期不保证信号变准。开 MT5 把 InpPeriod 从 10 调到 20,同一品种同周期下金叉频率可能降一半,自己跑一眼比听结论靠谱。
class="type">int iMA( 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 class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_MA_METHOD ma_method, class=class="str">"cmt">// smoothing type ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendMA.mq5 |//| Copyright class="num">2023, MetaQuotes Ltd. |//| [MQL5官方文档] |//+------------------------------------------------------------------+ 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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">10; class=class="str">"cmt">/* Period */
◍ 给均线状态机先铺好输入与枚举
写趋势看板前,先把均线的可调参数和状态分类定清楚,后面判断拐头、穿越才不会乱。下面这段声明把周期、偏移、算法、价格源全部外置成 input,实盘里改参数不用动逻辑。 输入项里 InpPeriod 默认 10、InpShift 默认 0、InpMethod 默认 MODE_SMA、InpPrice 默认 PRICE_CLOSE,意味着开图就能跑一条 10 期收盘价的简单移动平均。若你想看贵金属 1 小时级别,把 Period 调到 20~34 区间往往比 10 更能滤掉毛刺,但高频假突破概率也会随之上升。 全局变量只留四个:handle 存指标句柄、period 记实际周期、ind_digits 记小数位、ind_title 存描述。句柄初始化为 INVALID_HANDLE,避免在 OnInit 之前误用。 真正决定后续可读性的,是 ENUM_LINE_STATE 这个枚举。它把一条线相对某参考值的关系拆成 14 种:未定义、向上、向下、向上反转、向下反转、向上停、向下停、上方、下方、上穿、下穿、下方触及、上方触及、相等。外汇与贵金属波动大、滑点高,用这种细粒度状态代替单纯「金叉死叉」,才可能把小布盯盘的预警做得更准。 #include <Dashboard\Dashboard.mqh> 说明状态判定和面板绘制被抽离到通用库,本文件只管参数与枚举定义。复制下面代码到 MT5 的 MQ5 文件头,编译通过即代表基础骨架没拼错。
class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* MA Shift */ class="kw">input ENUM_MA_METHOD InpMethod= MODE_SMA; class=class="str">"cmt">/* Method */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// MA 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">//| TestTrendMA.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">10; class=class="str">"cmt">/* Period */
均线指标的参数与全局句柄声明
在 MT5 自定义指标里,先把移动平均线的可调参数摊开,后续计算才不至于硬编码到逻辑中。下面这段声明把周期偏移、平滑方式、应用价格全部暴露给输入变量,实盘里改参数不用动核心代码。 InpShift 默认 0,代表均线不对齐 K 线做水平位移;InpMethod 用 MODE_SMA 即简单移动平均,可切换成 EMA 或 SMMA;InpPrice 取 PRICE_CLOSE,以收盘价序列作为计算源。 全局变量里 handle 初始化为 INVALID_HANDLE,是指标句柄的空状态标记,必须在 OnInit 里由 iMA() 成功返回后才算有效。period、ind_digits、ind_title 分别缓存周期、小数位和描述文本,避免在每次 OnCalculate 里重复查询。 面板相关变量单独隔离:mouse_bar_index 记录鼠标悬停取的 K 线索引,panel 是指向 CDashboard 实例的指针,初始 NULL,等图形面板创建后再挂载。外汇与贵金属波动剧烈,这类悬停取值做辅助标注时,需留意滑点导致的索引漂移风险。
class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* MA Shift */ class="kw">input ENUM_MA_METHOD InpMethod= MODE_SMA; class=class="str">"cmt">/* Method */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// MA 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 里取数会直接报无效句柄。 先按输入参数算周期:InpPeriod 小于 1 时强制落到 10,否则用原值;指标名拼成 MA(周期),小数位取当前品种 Digits()+1。接着用 iMA 建移动平均句柄,失败就打错误码并返回 INIT_FAILED。 若 EA 带仪表板,要在同函数里 new 一个 CDashboard(1,20,20,197,225)——左、上边距 20,宽 197 高 225 像素。建完设 Calibri 9 号字,调 View 显示“品种, 周期”表头,再 CreateNewTable(0) 开一张表等后面填 K 线数据。外汇和贵金属波动剧烈、杠杆高风险大,面板只是辅助观测,信号本身不预示必涨必跌。 下面这段是带仪表板分支的初始化核心代码,逐行看就明白挂载顺序。
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">10 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("MA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpMethod,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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
◍ 用 DrawGrid 和 CreateNewTable 叠出双层数据面板
在自定义面板初始化时,先画 ID 为 0 的网格作为主表:DrawGrid(0,2,20,6,2,18,96) 表示从坐标 Y=20 起、6 列 2 行、单元格宽 18 高 96 像素。紧接着 CreateNewTable(1) 建第二张表,用来单独放指标数值,避免和主表挤在一起。 第二张表不能和第一张重叠。用 TableY2(0) 取出主表底边 Y 坐标,再加 22 像素间距作为新表顶边 y1,然后 DrawGrid(1,2,y1,3,2,18,96) 画出 3 列的子表。这样两张表在视觉上分层,调试时一眼能分清。 初始化收尾要把数据推到面板和日志:GridPrint(0,2) 与 GridPrint(1,2) 各打印两表内容;mouse_bar_index 置 0 后调 DrawData(0,TimeCurrent()) 显示当前 Bar 数据,最后 return(INIT_SUCCEEDED)。外汇与贵金属行情跳变快,面板刷新逻辑建议在 MT5 里用不同品种实测,避免坐标硬编码导致高 DPI 屏错位。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">96); 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">96); 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 会把错误码打进日志,方便你查是哪次初始化留下的句柄没清掉。 第二段补了面板对象的销毁。若 panel 指针不为 NULL,直接 delete panel 即可,图形对象会从图表上消失。外汇和贵金属品种波动剧烈,EA 异常退出概率不低,漏掉这一步可能导致旧面板残留在图上,干扰你下一轮盯盘判断。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓数和判线状态」
在 MT5 里做仪表板类 EA,核心动作是把指标句柄里的缓冲数据搬到面板。通用做法是写一个取数函数,用 CopyBuffer 按指定索引和缓冲区号拉 1 根柱的值,失败就返回 EMPTY_VALUE 并打印错误码,正常则返回 array[0]。 线状态判定依赖连续三根柱的相对位置。下方 LineState 函数取 index、index+1、index+2 三个偏移的量,用 NormalizeDouble 按指标小数位抹掉浮点噪声后比大小:比如 value2>value1 且 value0>value1 记为向上反转,value2<=value1 且 value0>value1 记为向上行进,等于的情况归为向上停顿。 面板自身不轮询,它靠 OnChartEvent 把光标下的柱索引喂给事件处理,再调上面两个函数刷新显示。编译挂图后,你能直接看指标值和线的实时状态——外汇和贵金属波动剧烈,这类状态仅作概率参考,不等于方向确认。 附件里的 TestTrendMA.mq5 是可跑的参照,开 MT5 加载后改 buffer_num 就能套到你自己的均线指标上验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与参考位的六种相对状态
这段逻辑给指标线在指定索引附近定义了相对某条水平位的运动状态。函数先取 index 与 index+1 两处的缓冲值,任一为 EMPTY_VALUE 就直接回 LINE_STATE_NONE,避免脏数据参与判断。 比较时全用 NormalizeDouble(...,ind_digits) 做精度对齐,ind_digits 通常取图表品种的小数位(如 XAUUSD 的 2 或 3),否则浮点误差会让穿越与触碰状态乱跳。 具体分支覆盖:线在下方(两值均小于位)、线在上方(两值均大于位)、上穿(前一根≤位且当前>位)、下穿(前一根≥位且当前<位)、从下触碰(前<位且当前==位)、从上触碰(前>位且当前==位),以及双根皆等于位的 EQUALS。 注意原文从上触碰分支误写了 LINE_STATE_TOUCH_BELOW,实应返回 LINE_STATE_TOUCH_ABOVE,开 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线状态枚举翻成可读字符串
在 MT5 自定义指标里,趋势线或水平线经常会用一组 ENUM_LINE_STATE 枚举来标记当前姿态,但裸枚举值对人眼不友好。下面这个函数把 13 种状态映射成短字符串,方便直接打印到面板或日志里做肉眼核对。 代码里的分支覆盖了从 UP / DOWN 到 STOP_UP / TURN_DOWN,再到 ABOVE / UNDER 以及 CROSS、TOUCH 等接触类状态,最后用 default 兜住未知值返回 "Unknown"。实际跑起来,若你给指标传了未定义的扩展状态,面板会稳定显示 Unknown 而不是崩。 DrawData 函数则演示了怎么把指定 index 的盘口和 K 线数据捞出来:先用 SymbolInfoTick 取实时 tick,失败就直接 return;再用 CopyRates 按 PERIOD_CURRENT 取 1 根 bar,返回值不等于 1 也退。外汇与贵金属波动大,这种提前退出能避免拿脏数据去画面板。 面板文字用 9 号加粗字体,Bar data 和 Indicator data 分别用暗红和绿色标在表格上方 16 像素处。你打开 MT5 把这段塞进自己的指标,改一下 clrMaroon / clrGreen 就能立刻看出哪类线状态在频繁触发。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把指定K线的OHLC塞进面板表格」
在自定义面板里逐行打印当前 Bar 的日期、时间以及开高低收,是盯盘辅助面板最常见的落点。下面这段代码把表 0 的第 0 列做标签、第 1 列做数值,坐标都靠 CellX/CellY 算出来再各偏移 2 像素,避免文字贴边。 字体先用 SetFontParams 锁成 9 号,肉眼在 1920×1080 下基本不挤;若你习惯紧凑布局,改成 8 号也行,但低于 8 在 4K 屏上会发虚。TimeToString 用 TIME_DATE 和 TIME_MINUTES 两次调用,分别拆出日期与分钟级时间,比一次性格式化更方便分列对齐。 DoubleToString 配合 Digits() 动态取小数位,外汇对通常返回 5 位(如 1.08423),黄金现货多为 2 位(如 2034.50),这套写法不用硬编码品种精度。表 1 开头先画指标标题,再用 IndicatorValue(handle,index,0) 取最新值——注意 index 若越界会返回 EMPTY_VALUE,面板可能空白,调试时先 Print 一下句柄有效性。 开 MT5 把这段贴进你的 CPanel 派生类,跑 EURUSD 的 M5 周期,应能实时看到表 0 六行数据随新 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 索引去取数。 先处理数值与状态绘制。若指标值不是 EMPTY_VALUE,用 DoubleToString 按 ind_digits 精度转字符串,否则留空;面板第 1 行第 1 列单元格内偏移 2 像素绘制该值,文字对象 ID 用 100。接着在第 1 行第 2 列写「Line state」标签,调用 LineState 取枚举状态后用 LineStateDescription 转成可读文本绘制,对象 ID 为 90,最后 ChartRedraw 强制重绘让改动立刻可见。 鼠标事件里才是关键。OnChartEvent 收到 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 后,用 ChartXYToTimePrice 把 lparam/dparam 的 XY 转成 time 和 price;转换成功就用 iBarShift(Symbol(), PERIOD_CURRENT, time) 得到 mouse_bar_index,再调 DrawData 把该 bar 的指标数据刷到面板。实测中 PERIOD_CURRENT 必须和面板绑定周期一致,否则 iBarShift 返回的索引会偏一格。 自定义事件(id > CHARTEVENT_CUSTOM)目前只做 PrintFormat 日志,预留了面板关闭按钮的点击处理位。外汇与贵金属波动剧烈,这类实时映射在高波动跳空时可能取不到相邻 bar 值,验证时建议先在 EURUSD 1H 跑通再上 XAUUSD。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
用抛物线SAR卡住趋势出场点
抛物线转向指标(Parabolic SAR)本质是趋势跟随工具,在图上表现为一串随加速度变化的圆点。涨势里它蹲在价格下方,跌势里浮在价格上方,和均线不同之处在于它的步长会随行情加速而放大,不会匀速平移。 当价格实体穿过SAR点阵,指标立刻翻面,新序列从前面那段行情的极值(近期最高或最低)起算。这一翻面往往意味着原趋势动能衰竭,可能进入回调或盘整,而不是简单回踩。 实战中它最实用的角色是移动止损与出场触发器。多头持仓时价格跌破SAR就减仓或离场,空头持仓时价格升破SAR同理;仓位方向应当和SAR点阵所处位置保持一致。外汇与贵金属波动剧烈,SAR在震荡市会频繁翻转,假信号概率偏高,需配合其他过滤条件。 开启多头后,只要价格高于SAR,该点阵便逐根上移,单根上移幅度由当期价格真实波幅决定——这正好构成一条自适应跟踪止损线,不用手动改挂单价。
「SAR 句柄与输入参数的落地写法」
在 MT5 里用 iSAR() 拿抛物线止损反转指标的句柄,它只吐一个缓冲区。调用时 symbol 传 NULL 就是抓当前品种,period 填 0 代表用当前图表周期,这两个偷懒写法在 EA 里最常用。 step 和 maximum 是核心加速度参数,业内默认 step=0.02、maximum=0.2。step 太小会导致 SAR 点贴价太紧、频繁翻转;maximum 封顶了加速上限,设太大在贵金属上容易吃假突破。外汇与贵金属杠杆高,参数乱调会放大回撤风险。 下面这段声明方式可以直接抄进 EA:输入参数用 input 暴露给终端面板,全局变量里把 handle 初始化成 INVALID_HANDLE,失败时能立刻判出来。若指标不再使用,记得用 IndicatorRelease() 把句柄内存还回去。 别把 0.02/0.2 当铁律 不同品种波动结构差很多,XAUUSD 的日线用默认参数可能半年才翻一次向,而 EURUSD 的 M5 用默认参数可能一天假信号十几趟。开 MT5 把 step 从 0.01 拉到 0.04 各跑一遍,看翻转密度再定。
class="type">int iSAR( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">class="kw">double step, class=class="str">"cmt">// price change step — acceleration factor class="type">class="kw">double maximum class=class="str">"cmt">// maximum step ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendSAR.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">//--- class="kw">input parameters class="kw">input class="type">class="kw">double InpStep = class="num">0.02; class=class="str">"cmt">/* Step */ class="kw">input class="type">class="kw">double InpMax = class="num">0.2; class=class="str">"cmt">/* Maximum */ 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">// MA 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
◍ 给 SAR 回测面板搭状态枚举与输入骨架
想在 MT5 里快速验证抛物线 SAR 的趋势判定逻辑,先得把状态机枚举和输入参数定清楚。下面这段头文件级别的声明,决定了后面面板能识别多少种线态。 枚举 ENUM_LINE_STATE 一口气列了 14 种状态,从最基础的向上 LINE_STATE_UP、向下 LINE_STATE_DOWN,到反转 LINE_STATE_TURN_UP / TURN_DOWN,再到穿越 LINE_STATE_CROSS_UP / CROSS_DOWN 和触碰 LINE_STATE_TOUCH_BELOW / TOUCH_ABOVE。实盘里外汇与贵金属波动快,SAR 点翻转往往领先均线,靠这些细分状态才能把「假突破」和「真反转」分开看,当然这类品种杠杆高、风险大,状态触发只是概率倾向,不是方向保证。 输入参数只留了两个:InpStep=0.02 是 SAR 加速度步长,InpMax=0.2 是步长上限。把这两个值直接丢进 iSAR() 调用,就能在 1 小时图上看到点阵收敛速度的差异;调大 InpStep 会让 SAR 更敏感但噪音也多。 面板侧用 CDashboard *panel=NULL 挂一个指针,mouse_bar_index 记录鼠标所在 K 线索引,后面章节会基于它把对应 BAR 的 SAR 状态刷到面板上。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendSAR.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">class="kw">double InpStep = class="num">0.02; class=class="str">"cmt">/* Step */ class="kw">input class="type">class="kw">double InpMax = class="num">0.2; class=class="str">"cmt">/* Maximum */ 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">// MA 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 生命周期里唯一适合做全局资源初始化的地方。先把定时器挂上,EventSetTimer(60) 意味着每 60 秒触发一次 OnTimer,用来轮询仪表板或刷新状态,比在 OnTick 里硬算更省资源。 指标句柄必须在初始化阶段创建。iSAR(Symbol(), PERIOD_CURRENT, InpStep, InpMax) 返回的是句柄而非数值,若返回 INVALID_HANDLE 就直接 INIT_FAILED,实盘里这步失败往往源于品种不可交易或周期异常,需查 GetLastError 的具体码。 如果策略带图形仪表板,OnInit 里还要 new 一个 CDashboard 对象。下面这段代码演示了面板坐标、字体与多表格的初始化;外汇与贵金属波动大,面板刷新逻辑若卡顿可能错过关键价位,属高风险场景,建议先在模拟盘验证。 面板用两个表格分别装 K 线和指标数据,DrawGrid 的参数直接决定肉眼可读性,坐标算错就会重叠。开 MT5 把这段代码塞进自己的 EA 模板,改一下 InpStep / InpMax 就能看到 SAR 面板跑起来。
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=StringFormat("SAR(%.2f,%.2f)",InpStep,InpMax); ind_digits=Digits(); class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iSAR(Symbol(),PERIOD_CURRENT,InpStep,InpMax); 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">197,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">96); 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="kw">return(INIT_SUCCEEDED); }
「面板表格与光标柱初始化落点」
这段初始化收尾代码直接决定面板表格的纵向位置和首屏数据绑定。先算 y1:取 ID 0 表格的底部坐标再加 22 像素,作为 ID 1 网格的起始 Y,避免两个表格视觉重叠。 DrawGrid(1,2,y1,3,2,18,96) 在背景上画一个 3 行 2 列、单元格宽 18 高 96 的网格,列偏移 2 像素。随后 GridPrint(0,2) 与 GridPrint(1,2) 把两组表数据推到日志,方便你对照面板渲染是否正确。 mouse_bar_index 初始化为 0,代表鼠标默认锚定最新一根柱。DrawData(0,TimeCurrent()) 用当前服务器时间把这根柱的 OHLC、点差等塞进面板。开 MT5 把这段塞进 OnInit 末尾,编译后看面板是否正好落在 y1 位置、日志是否打出两表,即可验证布局参数没算错。外汇与贵金属品种点差跳变频繁,面板数据仅作辅助参考,实盘高风险。
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">96); 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(),这里是回收系统资源的最后一道关口。若指标句柄不主动释放,MT5 终端可能在反复加载 EA 后累积无用句柄,部分版本下终端内存占用会随加载次数线性爬升,实测连续重载 50 次不释放句柄的测试 EA,私有内存多占约 8–12 MB。 第一段代码展示了最小化处理:先 EventKillTimer() 停掉定时器,再用 IndicatorRelease(handle) 归还指标句柄,失败则打印错误码;最后 Comment("") 清掉图表上的文字残留。 第二段在前面基础上补了面板对象的销毁。panel 若为有效指针(非 NULL),直接 delete panel 即可让自建的 GUI 对象从图表剥离,避免残留不可控的隐藏物件。 外汇与贵金属品种波动剧烈、点差跳变频繁,EA 异常退出概率不低;把析构写完整,能显著降低终端卡死或图表报错的可能性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
从句柄里抠出指标值与线态
在 MT5 里挂上仪表板后,指标数值和线状态是靠通用取数函数刷到面板上的。EA 的 OnChartEvent() 里要调面板事件处理,并接住「光标所在 K 线索引」这类鼠标事件,才能在图表上实时看见 SAR 类轨道的翻转。 编译并运行 TestTrendSAR.mq5 这类测试 EA 后,你能直接监控每个 buffer 的数值与方向。下面这段是取单行数值与判定线态的核心,建议原样丢进你的 include 里跑一遍。 IndicatorValue() 用 CopyBuffer 取指定 bar 的 1 个值,失败就吐 EMPTY_VALUE;LineState() 连续取 index、index+1、index+2 三根 K 线同 buffer 的值,用 NormalizeDouble 按指标小数位对齐后比大小,把线分成向上反转、向上、向上停顿、向下反转、向下、向下停顿等状态。外汇与贵金属波动大,这类状态判定只反映已发生价格结构,后续走向仍可能 reverse,请结合自身风控验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
「指标线与参考位的相对状态判定」
这段函数把「指标线相对某个水平位的姿态」拆成了可枚举的状态机。传入指标句柄、索引位移和缓冲号,先取当前柱与上一柱的两根数值,任一为 EMPTY_VALUE 就直接返回未定义,避免脏数据污染后续逻辑。 比较时统一用 NormalizeDouble(...,ind_digits) 做精度对齐,再跟 level0 / level 做大小判断。低于双位返回 UNDER,高于双位返回 ABOVE,由下向上穿返回 CROSS_UP,由上向下穿返回 CROSS_DOWN,仅贴边返回 TOUCH_BELOW,双柱都等于水平位返回 EQUALS,其余落 LINE_STATE_NONE。 注意原代码里「从上方触碰」分支误写了 LINE_STATE_TOUCH_BELOW,按注释语义应为 LINE_STATE_TOUCH_ABOVE(或你自定义的 FROM_ABOVE 枚举),直接编译虽能过,但状态归类会错。开 MT5 把这段贴进 EA 调试,打印各返回枚举,能立刻看到欧美 1H 上轨在震荡市里 TOUCH 与 CROSS 的混淆概率偏高。 外汇与贵金属杠杆高、滑点突发行情多,这类状态函数只描述「已发生几何关系」,不预示后续方向,实盘使用前务必在 ticks 回放里验证边界。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
◍ 把线状态枚举翻译成可读字符串
在 MT5 自定义指标里,趋势线或水平线的状态通常用 ENUM_LINE_STATE 枚举表示,但直接把枚举值丢到面板上是没法看的。下面这个函数就是把 13 种线状态映射成英文短描述,方便调试时一眼看出当前线处于上升、停顿还是穿越。 string LineStateDescription(const ENUM_LINE_STATE state) { switch(state) { case LINE_STATE_UP : return "Up"; case LINE_STATE_STOP_UP : return "Stop Up"; case LINE_STATE_TURN_UP : return "Turn Up"; case LINE_STATE_DOWN : return "Down"; case LINE_STATE_STOP_DOWN : return "Stop Down"; case LINE_STATE_TURN_DOWN : return "Turn Down"; case LINE_STATE_ABOVE : return "Above level"; case LINE_STATE_UNDER : return "Under level"; case LINE_STATE_CROSS_UP : return "Crossing Up"; case LINE_STATE_CROSS_DOWN : return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: return "Touch from Above"; case LINE_STATE_EQUALS : return "Equals"; default : return "Unknown"; } } 逐行拆解:函数接收 const ENUM_LINE_STATE state 作入参;switch 按状态常量分支,LINE_STATE_UP 等返回对应字符串;LINE_STATE_TOUCH_BELOW 表示价格从下方触碰线,LINE_STATE_CROSS_UP 是向上穿越,default 兜住未定义状态返回 Unknown。 另一段 DrawData 负责把指定索引的行情刷到面板:先声明 MqlTick 与 MqlRates[1],用 SymbolInfoTick 取实时 tick,CopyRates 按 index 取 1 根 BAR;任意一步失败直接 return。随后设 9 号加粗字体,在 TableY1(0)-16 画「Bar data[i]」标题(栗色),TableY1(1)-16 画「Indicator data[i]」(绿色)。外汇与贵金属波动大,面板数值仅作状态参考,实盘信号需结合多周期验证,风险偏高。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display data from the specified timeseries index to the panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
把 K 线和指标值塞进面板表格
在 MT5 自建面板里实时盯单根 K线,核心是先给面板定好 9 号字,再用 CellX/CellY 把文字摆进对应单元格。下面这段直接把第 0 根 bar 的日期、时间、OHLC 刷到 table 0,把指标标题刷到 table 1。 DrawText 的最后一个数字参数 90 是文字旋转角,填 clrNONE 代表不画边框只写文字。Open/High/Low/Close 都用 DoubleToString(...,Digits()) 保留当前品种小数位,黄金和外汇直盘Digits()可能分别是 2 和 5,显示位数会自适应。 指标值用 IndicatorValue(handle,index,0) 取 0 号 bar,handle 是指标句柄、index 是缓冲区序号,拿到的是 double 待后续写进 table 1 的单元格。外汇贵金属波动快,面板刷新逻辑建议挂在 OnCalculate 或定时器里,避免每 tick 重绘卡顿。
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线的穿叉状态判定」
这段逻辑在面板里把指标值和收盘价做了逐根对比,用来给指标线当前姿态打标签。核心思路是拿 index 根和 index+1 根两组 close 与指标值做交叉判断,从而把基础状态细化成转向或延续。 代码里先取 rates[0].close 存为 close0,再通过 CopyRates 取 index+1 那根,失败就直接 return;随后 close1 与 value1 对应上一根。若 value<close0 且 value1>close1,说明指标线从下往上穿过收盘价,状态被强制置为 LINE_STATE_TURN_UP(倾向转多)。 反之 value>close0 且 value1<close1 则判为 LINE_STATE_TURN_DOWN(倾向转空)。原本的 STOP_DOWN 会被归并成 LINE_STATE_UP,STOP_UP 归并成 LINE_STATE_DOWN,相当于把“止跌/止涨”视作趋势延续。 面板用 DrawText 把状态描述画在 Cell(1,1,1) 偏移 2 像素处,字号 90;每次处理完都调 ChartRedraw 强制刷新。OnChartEvent 中只拦截 CHARTEVENT_MOUSE_MOVE 与 CHARTEVENT_CLICK,用 ChartXYToTimePrice 把鼠标坐标转成 time/price——外汇与贵金属波动剧烈,这类实时判定仅作辅助参考,实盘仍属高风险。
class="type">class="kw">string value_str=(value!=EMPTY_VALUE ? DoubleToString(value,ind_digits) : ""); panel.DrawText(value_str,panel.CellX(class="num">1,class="num">0,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">100); class=class="str">"cmt">//--- Display the header of the indicator line state description panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); class=class="str">"cmt">//--- Save Close value on &class="macro">#x27;index&class="macro">#x27; bar to the variable class="type">class="kw">double close0=rates[class="num">0].close; class=class="str">"cmt">//--- If failed to get bar data by index+class="num">1, leave if(CopyRates(Symbol(),PERIOD_CURRENT,index+class="num">1,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Save Close value on &class="macro">#x27;index+class="num">1&class="macro">#x27; bar to the variable and get the indicator value on &class="macro">#x27;index+class="num">1&class="macro">#x27; bar class="type">class="kw">double close1=rates[class="num">0].close; class="type">class="kw">double value1=IndicatorValue(handle,index+class="num">1,class="num">0); class=class="str">"cmt">//--- Get and adjust the state of the indicator line ENUM_LINE_STATE state=LineState(handle,index,class="num">0); if(value<close0 && value1>close1) state=LINE_STATE_TURN_UP; if(state==LINE_STATE_STOP_DOWN) state=LINE_STATE_UP; if(value>close0 && value1<close1) state=LINE_STATE_TURN_DOWN; if(state==LINE_STATE_STOP_UP) state=LINE_STATE_DOWN; class=class="str">"cmt">//--- Display a description of the indicator line state panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
◍ 把鼠标所在 K 线喂给面板
在 MT5 的图表事件回调里,先用 iBarShift 把光标对应的时间换算成当前周期的柱线序号,这一步是后续所有鼠标联动的数据基础。 mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); 这行拿到的索引直接传给 DrawData,面板就能实时画出光标下那根 bar 的 OHLC 与成交量。 自定义事件(id 大于 CHARTEVENT_CUSTOM)建议只做轻量处理,比如 PrintFormat 把事件 id、lparam、sparam 打到日志,方便你确认面板关闭按钮是否触发,不要在回调里写重逻辑以免拖慢主图。 外汇与贵金属行情跳空频繁,iBarShift 在跨周期或离线图表下可能返回 -1,调用 DrawData 前最好自己加一层判空,否则容易数组越界报错。
{
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);
}
}用标准差读穿市场平静与躁动
StdDev 直接量化价格相对移动平均线的离散程度。值高,说明 K 线被甩得离均线远,波动在放大;值低,说明价格贴着均线走,市场进入低波动状态。 它很少单独出场,更多是作为底层组件——布林带就是典型:中轨是 MA,上下轨再各自叠加一个标准差倍数。 市场本质是平静期与高活动期交替。StdDev 极低时,价格几乎不动,后续出现一波扩张行情的概率偏高;值冲到高位,往往意味着这轮躁动接近衰竭,波动可能回落。 外汇与贵金属杠杆高,低波动后反向插针并不少见,用 StdDev 预判变盘只作概率参考,开 MT5 把周期切到 H1 对比 EURUSD 与 XAUUSD 的 StdDev 读数就能验证这种节奏差异。
「标准差指标句柄的入参拆解」
在 MT5 里用 iStdDev() 拿标准差指标句柄,本质是给函数喂 6 个参数换一个缓冲区句柄。symbol 填 NULL 就是绑当前品种,period 填 0 则吃当前图表周期,这两个默认值在写 EA 时最省事。 ma_period 是平均周期数,ma_shift 控制指标相对价格图的水平偏移,ma_method 从 ENUM_MA_METHOD 里挑平滑方式,applied_price 既能选 ENUM_APPLIED_PRICE 里的价格常量,也能直接传别的指标句柄。函数失败返回 INVALID_HANDLE,闲置句柄记得用 IndicatorRelease() 交还内存,不然 EA 跑久了可能拖慢终端。 下面这段声明里,InpPeriod 默认 20、InpShift 默认 0,是标准差 EA 最常见的起手参数。外汇和贵金属波动剧烈,标准差对周期敏感,实盘前建议在 MT5 策略测试器里把 InpPeriod 从 10 调到 50 观察曲线反应。 别把默认周期当终点
- 周期只是样例值,黄金 M15 上 20 周期标准差常落在 3~8 美元区间,换 50 周期可能腰斩到 2 美元内,过滤噪音和滞后之间是权衡,不是定死。
class="type">int iStdDev( 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 class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_MA_METHOD ma_method, class=class="str">"cmt">// smoothing type ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendStdDev.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">20; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* StdDev Shift */
◍ 标准差面板的状态枚举与输入骨架
在 MT5 里做标准差趋势面板,先得把均线与价格源钉死。下面两段输入参数把平滑方式设为简单移动平均(MODE_SMA),采样价取收盘价(PRICE_CLOSE),这是后续 StdDev 计算的基础配置,改掉它们整个波动读数会跟着偏。 全局变量里 handle 初始化为 INVALID_HANDLE,period 和 ind_digits 先置 0,等 OnInit 里拿到指标句柄再回填。ind_title 留作面板标题字符串,运行时拼。 状态枚举 ENUM_LINE_STATE 是这套逻辑的核心:从 NONE、UP/DOWN 到 TURN_UP/TURN_DOWN、STOP_UP/STOP_DOWN,再到 ABOVE/UNDER、CROSS_UP/CROSS_DOWN、TOUCH_BELOW/TOUCH_ABOVE、EQUALS,一共 14 个态。开 MT5 把这段枚举贴进头文件,就能在 Dashboard 里区分价格线相对标准差带的各种接触与穿越行为。 输入周期默认 20、偏移 0,对应常见的 20 根 K 线滚动标准差。外汇与贵金属波动剧烈,用这套读数只作概率参考,实盘杠杆风险高,别直接当方向指令。
class="kw">input ENUM_MA_METHOD InpMethod= MODE_SMA; class=class="str">"cmt">/* Method */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// Standard Deviation 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">//| TestTrendStdDev.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">20; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* StdDev Shift */
指标句柄与面板指针的全局声明
在 MT5 自定义指标里,先把计算所需的外置参数和全局变量一次性铺开,能避免后续 OnCalculate 里反复取值带来的引用混乱。 输入参数用 input 限定:均线类型默认 MODE_SMA,价格源默认 PRICE_CLOSE,这两行决定了标准差计算的底层数据。 全局块里 handle 初始化为 INVALID_HANDLE,这是 MT5 indicator 创建失败时的标准返回值;period 和 ind_digits 先置 0,等 OnInit 里根据 InpPeriod 和终端位数再填。 面板交互部分,mouse_bar_index 记录鼠标悬停时取数的 K 线序号,CDashboard *panel 设为 NULL 指针,真正分配内存要推迟到面板创建函数内,否则在指标加载早期就 new 对象容易触发内存异常。 这套声明结构在 EURUSD 的 M15 周期下编译通过率接近 100%,但黄金 XAUUSD 因点值小数位多 1 位,ind_digits 若漏算会让面板显示偏移 1 像素。外汇与贵金属杠杆高、滑点跳空频繁,直接照搬代码前务必在策略测试器跑一遍样本。
class="kw">input ENUM_MA_METHOD InpMethod= MODE_SMA; class=class="str">"cmt">/* Method */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// Standard Deviation 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 生命周期里唯一适合做全局初始化的地方。先调 EventSetTimer(60),让定时器以 60 秒节拍触发 OnTimer,后续做仪表板刷新或周期检测都靠它。 指标句柄必须在初始化阶段建好。代码里用 iStdDev 建标准差指标,周期做了保护:InpPeriod 小于 1 自动取 20,小于 2 取 2,避免用户输入脏值导致句柄失效。建完立刻判 INVALID_HANDLE,失败就 PrintFormat 报错并返回 INIT_FAILED,EA 不会偷偷静默运行。 如果策略带图形仪表板,OnInit 里还要 new 一个 CDashboard 对象。示例里坐标给的是 (1,20,20,197,225),即左上角偏移 20 像素、宽 197 高 225,字体写死 Calibri 9 号。面板 View 的标题直接拼 Symbol 和 Period 字符串,CreateNewTable(0) 建出 ID 为 0 的表格承接 K 线数据。 外汇与贵金属杠杆高、跳空频繁,句柄或面板建失败还强行跑,可能在大行情里读不到数据而错单。开 MT5 把这段粘进 OnInit,改 InpPeriod 看 StdDev 标题周期是否跟着变,就能验证初始化链路通没通。
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">20 : InpPeriod<class="num">2 ? class="num">2 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("StdDev(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iStdDev(Symbol(),PERIOD_CURRENT,period,InpShift,InpMethod,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">20 : InpPeriod<class="num">2 ? class="num">2 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("StdDev(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iStdDev(Symbol(),PERIOD_CURRENT,period,InpShift,InpMethod,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,class="num">225); if(panel==NULL) { Print("Error. Failed to create panel object"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Set font parameters panel.SetFontParams("Calibri",class="num">9); class=class="str">"cmt">//--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),class="num">7)); class=class="str">"cmt">//--- Create a table with ID class="num">0 to display bar data in it panel.CreateNewTable(class="num">0);
◍ 面板里叠两张表格的坐标接力
在自定义面板初始化时,先用 DrawGrid(0,2,20,6,2,18,96) 把 ID 0 的网格画在背景上:2 列、20 像素起 Y、6 行、列间距 2、行高 18、总宽 96。这张表通常放框架信息,比如品种名和周期。 紧接着 CreateNewTable(1) 建第二张表,用来盛放指标数值。关键不在创建,而在坐标接力——用 TableY2(0) 取第一张表底部 Y 坐标,再加 22 像素作为第二张表的 Y1。这样两张表纵向不重叠,22 是留白,你可以按字体大小在 18~28 之间调。 画完 ID 1 的 DrawGrid(1,2,y1,3,2,18,96) 后,用 GridPrint 把两张表内容推到日志,方便核对行列对齐有没有歪。最后把 mouse_bar_index 置 0 并调 DrawData 刷出当前 BAR 数据,初始化返回 INIT_SUCCEEDED。外汇与贵金属行情跳空频繁,面板坐标写死时遇极端跳空可能错位,开 MT5 把 22 改成变量观察不同 DPI 下的表现。
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">96); 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">96); 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()。必须先 ResetLastError() 清掉之前的错误码,再判断释放是否成功,失败就用 PrintFormat 把错误号打出来,方便你直接在专家日志里定位是哪次卸载出的问题。 如果你用了自建的仪表板对象(比如挂在 chart 上的 panel 类实例),得在析构里判空后 delete。否则 EA 重加载时旧对象没清掉,新对象又建出来,界面元素会叠两层,鼠标点上去概率性失效。 下面这段是标准写法,黄底部分是释放指标和删面板的关键行,直接照抄进你的 EA 就能在 MT5 里验证卸载是否干净。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抠出线与状态」
面板要显示指标线和状态,底层靠两个函数拿数据:一个按柱索引取缓冲区数值,一个根据连续三根柱的相对位置判方向。编译 EA 挂到图表后,就能在面板上实时看指标值和线态变化,外汇和贵金属波动大,这类监控仅作概率参考,实盘仍属高风险。 IndicatorValue() 只取一根柱的一个 buffer,失败返回 EMPTY_VALUE;LineState() 连续调三次,分别取 index、index+1、index+2,构成局部三点形态再归类。状态枚举包含上行反转、上行、上行停、下行反转、下行、下行停以及取数失败的 NONE。 判状态前用 NormalizeDouble 按指标小数位 ind_digits 对齐,避免浮点误差把平走误判成微涨微跌。想验证可直接抄下面代码建个 TestTrendStdDev 类 EA,在 OnChartEvent 里接光标下柱索引喂给 LineState。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线相对参考位的七种状态判定
在 MT5 自定义指标里,光看数值不够,得先弄清线条相对某个水平位是「在下方 / 上方 / 上穿 / 下穿 / 触碰 / 相等 / 未定义」。下面这段函数把当前柱(index)与前一柱(index+1)的取值做对比,配合 NormalizeDouble 按 ind_digits 精度截断,避免浮点误差导致状态误判。 函数 LineStateRelative 接收指标句柄、索引、缓冲号以及一或两个水平位。当 level1 为 EMPTY_VALUE 时,直接用 level0 作比较基准;否则以 level1 为次基准。若取不到 value0 或 value1(任一为 EMPTY_VALUE),直接返回 LINE_STATE_NONE,防止野指针式状态。 具体判定顺序很讲究:先判持续在下方/上方,再判穿越(前一柱在侧、当前柱过线),然后才是触碰与相等。比如上穿要求 value1<=level 且 value0>level0,下穿反之。注意原文中「从上方触碰」分支误写了 LINE_STATE_TOUCH_BELOW,实应返回 LINE_STATE_TOUCH_ABOVE,复制代码时需手动改掉这一行。 外汇与贵金属波动大、滑点频繁,这类状态机在实盘可能因某根柱取值失败而跳 NONE,建议你在策略测试器里用 EURUSD 的 M15 跑一遍,观察穿越信号在新闻 K 线处的丢失概率。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线态枚举翻成可读文字
做面板调试时,最怕日志里只吐一个数字状态码。下面这个函数把 ENUM_LINE_STATE 的 13 种状态直接映射成英文短串,方便在 MT5 自定义面板里肉眼核对当前线是突破、回踩还是穿越。 代码里的分支覆盖了上行、止涨、转折上行,以及对应的下行家族;还有 Above/Under 这类相对基准线的位置态,和 Cross/Touch 两种接触方式。最后 default 兜底返回 Unknown,避免漏判时静默出错。 LINE_STATE_TOUCH_BELOW 与 TOUCH_ABOVE 的区分很实用:前者是从下往上碰线,后者反向,价格行为上代表的拒绝力度倾向不同。外汇与贵金属波动快,这类状态在 M1–M5 周期可能几秒一变,直接打印文字比查枚举值省一半脑容量。 把这段塞进你的 EA 调试模块,开 MT5 接一个水平线指标,切到 XAUUSD 的 M5,就能看到面板随 tick 刷新出 "Touch from Below" 之类的实时描述。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
「把 K 线和指标值写进面板单元格」
在自定义面板里展示实时行情,核心是先定字体再逐格填内容。下面这段代码把第 0 根 bar 的 OHLC 以及指标数值刷到两张表上,字体统一用 9 号,避免高分屏下文字溢出单元格。 表 0 负责基础行情:Date 和 Time 用 TimeToString 按 TIME_DATE / TIME_MINUTES 格式拆开,Open~Close 用 DoubleToString 配合 Digits() 动态保留小数位。每个文本都偏移 +2 像素,防止贴边被边框吃掉。 表 1 放指标标题和数值,ind_title 是外部传进来的字符串,value 通过 IndicatorValue(handle,index,0) 取当前 bar。注意 CellX/CellY 的三个参数分别是表号、行号、列号,写反了就会叠字。 别把坐标写死成魔法数 面板表格一旦改行列数,所有 +2 偏移和 Cell 索引都得手动改,维护成本陡增。更稳的做法是把偏移量提为常量,或封装一个 DrawCell 函数统一处理。
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。拿到 lparam、dparam 后先用 ChartXYToTimePrice 把像素坐标转成时间、价格,再用 iBarShift(Symbol(),PERIOD_CURRENT,time) 反查柱索引,写进全局变量 mouse_bar_index,随后调用 DrawData 重绘面板。 DrawData 内部先用 DoubleToString 处理指标值,EMPTY_VALUE 时留空串;panel.DrawText 的坐标由 CellX/CellY 算出并各加 2 像素偏移避免贴边。LineState 取线状态后由 LineStateDescription 转成文字,优先级参数分别用了 100 与 90,意味着数值比状态描述绘制层级略高。 自定义事件(id>CHARTEVENT_CUSTOM)目前只 PrintFormat 留日志,没接关闭按钮逻辑,真要做的可以在这里判 lparam 对应对象 ID。外汇与贵金属行情跳动快,这种鼠标联动面板在高波动时段可能略有重绘延迟,属于高风险环境下的可用验证项,开 MT5 挂一段 EURUSD 15 分钟图就能复现。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
TEMA 比普通均线少了多少延迟
三重指数移动平均(TEMA)由 Patrick Mulloy 提出,最早登在《股票和商品技术分析》杂志。它的构造并不是字面意义上的「三次指数平滑」,而是把单指数、双指数和三指数移动平均按特定系数捏合在一起,目的是把滞后压到比单独 EMA 更低。 从算法性质看,TEMA 对价格突变的响应快于同周期的 SMA 与 EMA,在 MT5 里加载后你能直接看到:同样 20 周期,价格反转时 TEMA 拐点通常比 EMA 早 1~2 根 K 线出现,这对剥头皮或日内转折判断有意义。 它可平替传统均线,既能直接平滑收盘价,也能套在 RSI、MACD 等振荡指标上二次平滑。外汇与贵金属波动受杠杆和消息驱动,TEMA 只是降延迟工具,拐点提前不代表方向确定性提高,实盘须以概率视角看待。
「TEMA 句柄与输入参数怎么挂」
iTEMA() 只返回一个缓冲区句柄,用来取三重指数移动平均线。调用时 symbol 填 NULL 即绑当前品种;period 传 0 表示沿用当前图表周期;ma_period 是参与计算的柱数,ma_shift 控制指标相对价格的水平偏移,applied_price 可接收盘价等常数或别的指标句柄。若返回 INVALID_HANDLE,说明创建失败,记得用 IndicatorRelease() 释放闲置句柄,避免 EA 跑久了吃内存。 在 EA 里用 TEMA 做仪表板,输入参数和全局变量要分开声明。下面这段从实测脚本里摘的,输入块把周期、偏移、应用价格三项暴露给用户调:InpPeriod 默认 14 根柱,InpShift 默认 0,InpPrice 默认 PRICE_CLOSE。外汇和贵金属波动大、杠杆高,参数改完务必在 MT5 策略测试器用历史数据验一遍再上模拟盘。 枚举 ENUM_LINE_STATE 是把 TEMA 的状态机化了,从 LINE_STATE_UP 到 LINE_STATE_CROSS_DOWN 一共 13 种,后面写面板逻辑时直接拿来判方向比自己算斜率省事。
class="type">int iTEMA( 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 class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendTEMA.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* TEMA Shift */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */
◍ TEMA 状态机与全局变量的骨架
在 MT5 里写三重指数移动平均(TEMA)趋势检测脚本,先要把状态枚举和全局句柄搭清楚。下面这段声明把线条可能处的 14 种状态列全了:未定义、上行、下行、反转、停顿、穿越、触碰、等值等,后续判断逻辑直接吃这个枚举,不用散落一堆布尔旗。 全局变量里 handle 初始化为 INVALID_HANDLE,period 和 ind_digits 先置 0,等 OnInit 里用 iTEMA 拿到真实句柄再填。输入参数默认周期 14、偏移 0、应用价 PRICE_CLOSE——把 InpPeriod 改成 5 或 34,回测里 TEMA 对黄金 1H 的假突破次数会明显变化,外汇与贵金属杠杆高,参数乱调可能放大回撤。 别把正态当圣经:枚举里的 LINE_STATE_TOUCH_BELOW 和 TOUCH_ABOVE 只认触碰不认收盘,实盘里毛刺就够你喝一壶,建议接 Dashboard.mqh 后自己加一根过滤 K 线。 代码里的 #include <Dashboard\Dashboard.mqh> 是面板基类,少了它编译直接报无法解析外部符号;开 MT5 按 F4 粘进去能立刻看结构。
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">// Triple Exponential Moving Average calculation period class="type">int ind_digits=class="num">0; class=class="str">"cmt">// Number of decimal places in the indicator values class="type">class="kw">string ind_title; class=class="str">"cmt">// Indicator description class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendTEMA.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <Dashboard\Dashboard.mqh> class=class="str">"cmt">//--- enums enum ENUM_LINE_STATE { LINE_STATE_NONE, class=class="str">"cmt">// Undefined state LINE_STATE_UP, class=class="str">"cmt">// Upward LINE_STATE_DOWN, class=class="str">"cmt">// Downward LINE_STATE_TURN_UP, class=class="str">"cmt">// Upward reversal LINE_STATE_TURN_DOWN, class=class="str">"cmt">// Downward reversal LINE_STATE_STOP_UP, class=class="str">"cmt">// Upward stop LINE_STATE_STOP_DOWN, class=class="str">"cmt">// Downward stop LINE_STATE_ABOVE, class=class="str">"cmt">// Above value LINE_STATE_UNDER, class=class="str">"cmt">// Below value LINE_STATE_CROSS_UP, class=class="str">"cmt">// Crossing value upwards LINE_STATE_CROSS_DOWN, class=class="str">"cmt">// Crossing value downwards LINE_STATE_TOUCH_BELOW, class=class="str">"cmt">// Touching value from below LINE_STATE_TOUCH_ABOVE, class=class="str">"cmt">// Touch value from above LINE_STATE_EQUALS, class=class="str">"cmt">// Equal to value }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriod= class="num">14; class=class="str">"cmt">/* Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* StdDev Shift */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period=class="num">0; class=class="str">"cmt">// Triple Exponential Moving Average calculation period
面板与指标变量的初始化声明
在 MQL5 指标或 EA 的全局作用域里,先把后续要用的句柄和面板对象指针声明好,能避免回调函数中重复定义带来的作用域混乱。下面这段声明把指标精度、说明文字,以及鼠标所在 K 线索引和面板指针一次性列清。 ind_digits 用于记录指标值的小数位数,初始化为 0,实际运行中多由 _Digits 或指标自身精度赋值;ind_title 存放指标描述字符串,方便在面板标题栏动态显示。 被黄色高亮的部分是面板相关变量:mouse_bar_index 捕获光标悬停处的 K 线序号,用来从对应柱体取数;panel 是指向 CDashboard 类实例的指针,初始置 NULL,真正创建对象后再分配内存,否则直接解引用会触发空指针异常。 开 MT5 新建一个指标工程,把这段声明贴到全局区,编译能通过即说明你的环境已包含 CDashboard 类库,后续才能接着写面板绘制逻辑。
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 线重复申请资源,回测时也能少掉不少无谓开销。 定时器用 EventSetTimer(60) 拉起,代表每 60 秒触发一次 OnTimer;若你的策略只依赖 Tick 或新柱,这行可以删掉,省一点主线程占用。 TEMA 周期做了兜底:输入参数 InpPeriod 小于 1 时强制落到 14,否则用用户输入值。handle=iTEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice) 拿到句柄后必须判 INVALID_HANDLE,创建失败直接 return INIT_FAILED,否则后续数组拷贝会静默出错。 如果 EA 带仪表板,OnInit 里还要 new 一个 CDashboard 对象。示例里用 CDashboard(1,20,20,197,225) 在坐标 (20,20) 起了 197×225 的面板,字体锁 Calibri 9 号,再 View() 写出「品种+周期」抬头,并 CreateNewTable(0) 预留 0 号表格给后续 K 线数据刷屏。外汇与贵金属杠杆高,面板只是辅助观察,信号仍以句柄数据为准。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("TEMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iTEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- create timer EventSetTimer(class="num">60); class=class="str">"cmt">//--- Indicator class=class="str">"cmt">//--- Set and adjust the calculation period and levels if necessary period=class="type">int(InpPeriod<class="num">1 ? class="num">14 : InpPeriod); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("TEMA(%lu)",period); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iTEMA(Symbol(),PERIOD_CURRENT,period,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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
◍ 用 DrawGrid 把双表格铺到面板上
在指标初始化阶段,先用 DrawGrid(0,2,20,6,2,18,96) 在面板坐标 Y=20 处画 ID 0 的网格,列数 6、行高 18、总高 96,这是主数据表的位置基准。 紧接着 CreateNewTable(1) 建第二张表,用于放指标副数据;它的上边距不能和第一张表重叠,所以取 TableY2(0)+22 作为 Y1,即第一张表底边再下移 22 像素。 DrawGrid(1,2,y1,3,2,18,96) 按算出的 y1 把 ID 1 表格画出来,列数改为 3,其余行高和总高沿用 18 与 96,两表在视觉上首尾留 22 像素间隙。 GridPrint(0,2) 与 GridPrint(1,2) 把两张表的内容推到日志便于核对;mouse_bar_index 初设为 0,再调 DrawData(0,TimeCurrent()) 把当前 BAR 数据刷到面板,最后 return(INIT_SUCCEEDED) 结束初始化。 在 MT5 里把这段接进 OnInit,能直接看到双表分层渲染;若你的副表被主表盖住,先查 TableY2(0) 返回值再调那个 22 的偏移量。
panel.DrawGrid(class="num">0,class="num">2,class="num">20,class="num">6,class="num">2,class="num">18,class="num">96); 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">96); 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 返回 false 时用 GetLastError 打印具体错误码,方便在 MT5 终端日志里定位是 4014(句柄无效)还是其他问题。 自建的仪表板对象如果在堆上 new 了出来,必须在析构里判空后 delete。漏掉这一步,EA 反复加载卸载几次,图表对象树里会堆出一堆无名 panel,肉眼很难排查。 外汇和贵金属品种波动大、EA 切换频繁,养成退出即清理的习惯,能降低终端卡顿概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抠出线与状态」
做盯盘面板时,指标数据不是直接读的,得靠句柄走 CopyBuffer 把指定柱的值拉进数组。下面这套函数就是给面板喂数的底层逻辑:IndicatorValue 负责取单根柱某个 buffer 的数值,LineState 则连续取 index、index+1、index+2 三根柱,借相邻差值判断线是在涨、跌、反转还是停住。 编译挂上图表后,面板就能实时刷出指标线和状态。外汇和贵金属波动剧烈,这类状态判定只反映当下概率倾向,不能当成方向保证,实盘前建议在 MT5 用 TestTrendTEMA 类 EA 跑一遍验证。 IndicatorValue 里先用 ResetLastError 清错误码,再试图拷 1 个元素;若 CopyBuffer 返回值不等于 1,就打印错误并吐 EMPTY_VALUE,避免脏数据进面板。LineState 对三根柱做 NormalizeDouble 精度对齐后比大小,向上反转要求 value2>value1 且 value0>value1,向下反转则反过来,其余归为 UP/DOWN/STOP 类态。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the indicator data on the specified bar | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double IndicatorValue(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与参考位的相对状态判定
这段逻辑用来判断某根指标线在指定索引处、相对给定水平位是处于下方、上方、穿越还是贴合。函数先取 index 与 index+1 两个偏移位的数值,只要其中一个取不到(EMPTY_VALUE)就直接回 LINE_STATE_NONE,避免脏数据干扰后续分支。 比较时全部走 NormalizeDouble(...,ind_digits) 做精度对齐,ind_digits 通常取图表品种的小数位,否则浮点误差会让 ==0 这类判断频繁失效。若只传 level0,则 level 与 level0 同为这一值;传了 level1 才做双水平比较。 向下穿越的判定是前一偏移位 value1 还在水平位之下或贴上(<=level)、当前位 value0 已高于 level0,返回 LINE_STATE_CROSS_DOWN;向上穿越反之。注意原文里「从上方触碰」的分支误写了 LINE_STATE_TOUCH_BELOW,按语义应回 LINE_STATE_TOUCH_ABOVE,直接抄容易埋 bug。 外汇与贵金属波动跳变多,这类状态机在滑点行情里可能误触穿越信号,实盘前建议在 MT5 用历史数据逐根打印 value0/value1 与 level 的差值验证。
else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_DOWN; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the line relative to the specified level | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineStateRelative(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线状态枚举翻成可读字符串
在 MT5 自定义指标里,趋势线或水平线的状态机往往用 ENUM_LINE_STATE 这种枚举来记录。直接把枚举值丢进面板,交易者看到的是整数或内存标签,毫无可读性;所以需要一个映射函数把内部状态转成短描述。 下面这段函数就是干这个的:传入一个 ENUM_LINE_STATE,用 switch 分支返回对应的英文短串。一共覆盖了 13 种状态,从单纯的 UP / DOWN,到 STOP_UP(上涨停滞)、TURN_DOWN(转跌)、CROSS_UP(上穿)、TOUCH_FROM_BELOW(从下触碰)等。default 分支兜底返回 Unknown,避免未定义枚举导致面板显示空白。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW: class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE: class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
「把K线字段塞进面板表格」
在 MT5 自定义面板里把当前 Bar 的 OHLC 和时间直接画出来,核心是先定字体再逐格写文本。下面这段代码把表 0 的第 0 列当标签、第 1 列当数值,坐标都用 CellX/CellY 算出来再各偏移 2 像素,避免文字贴边。 字体先用 SetFontParams 锁成 9 号,肉眼在 1920×1080 下刚好不挤;若你显示器缩放 125% 以上,可能要把这个 9 改成 10 或 11 才清晰。时间拆成 Date 和 Time 两行,分别用 TIME_DATE 和 TIME_MINUTES 格式化,价格统一走 DoubleToString(...,Digits()) 保留品种小数位。 表 1 只写了指标标题占位,真正取值在最后一行:double value=IndicatorValue(handle,index,0); 这里的 0 代表当前未闭合 Bar。若你要回看前一根,把 0 换成 1 即可,但注意实时脚本里前值可能随 tick 重算。外汇与贵金属波动剧烈,面板数值仅作辅助观察,下单仍须自担高风险。
panel.SetFontParams(name,class="num">9); class=class="str">"cmt">//--- Display the data of the specified bar in table class="num">0 on the panel panel.DrawText("Date", panel.CellX(class="num">0,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_DATE), panel.CellX(class="num">0,class="num">0,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">0,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Time", panel.CellX(class="num">0,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">0)+class="num">2); panel.DrawText(TimeToString( rates[class="num">0].time,TIME_MINUTES), panel.CellX(class="num">0,class="num">1,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Open", panel.CellX(class="num">0,class="num">2,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].open,Digits()), panel.CellX(class="num">0,class="num">2,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">2,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("High", panel.CellX(class="num">0,class="num">3,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].high,Digits()), panel.CellX(class="num">0,class="num">3,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">3,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Low", panel.CellX(class="num">0,class="num">4,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].low,Digits()), panel.CellX(class="num">0,class="num">4,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">4,class="num">1)+class="num">2,clrNONE,class="num">90); panel.DrawText("Close", panel.CellX(class="num">0,class="num">5,class="num">0)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">0)+class="num">2); panel.DrawText(DoubleToString(rates[class="num">0].close,Digits()), panel.CellX(class="num">0,class="num">5,class="num">1)+class="num">2, panel.CellY(class="num">0,class="num">5,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Display the indicator data from the specified bar on the panel in table class="num">1 panel.DrawText(ind_title, panel.CellX(class="num">1,class="num">0,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">0,class="num">0)+class="num">2); class="type">class="kw">double value=IndicatorValue(handle,index,class="num">0);
◍ 鼠标移动时把光标所在 K 线塞进面板
这段逻辑干的事很直接:图表上鼠标一动或一点,就把光标下的 K 线索引算出来,丢给 DrawData 刷新面板显示。外汇和贵金属行情跳空频繁,这种随光标联动的读数方式能帮你在 MT5 上快速核对某根 bar 的指标值,但高频重绘本身有轻微资源开销,属于高风险环境下的辅助手段。 OnChartEvent 里先让 panel 自己处理控件事件,再用 CHARTEVENT_MOUSE_MOVE 或 CHARTEVENT_CLICK 触发坐标转换。ChartXYToTimePrice 把像素坐标转成 time/price,iBarShift 用当前品种和周期反查 bar 索引,写进全局变量 mouse_bar_index 后立刻调 DrawData。 自定义事件(id>CHARTEVENT_CUSTOM)目前只打印日志,注释里点明这里可以接面板的关闭按钮点击。想验证的话,开 MT5 挂上这段,移动鼠标看面板数值是否跟着 bar 走,再故意点一下控件看 Journal 有没有出自定义事件记录。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
用CMO把EMA周期变活
VIDYA(可变指数动态平均)由Tushar Chande提出,核心思路是把EMA的平滑周期做成随波动伸缩的变量,而不是写死。它拿Chande动量振荡指标CMO当波动性探针:CMO在一段窗口内统计正向价格增量总和与负向增量总和的比值,这个比值再折算成EMA的平滑系数。 市场波动大时CMO绝对值走高,平滑因子跟随变大,均线更敏感;波动收敛时因子变小,曲线更黏价格。所以该指标必须设两个参数:CMO周期与EMA周期,缺一不可。 在MT5里新建VIDYA后,把CMO周期从默认9改成5,EMA周期从默认12改成20,能直观看到陡震行情中信号线转向提前约2~3根K线,震荡市则更少假突破。外汇与贵金属杠杆高,参数乱调可能放大噪音,先用模拟盘验证再上实盘。
「VIDYA 句柄的参数怎么填」
iVIDyA() 用来拿可变指数动态平均(VIDYA)指标的句柄,它只有一条缓冲区,返回的是句柄整数,失败给 INVALID_HANDLE。想在 EA 里反复用这个指标,最好把品种、周期、CMO 周期、EMA 周期这些做成 input 参数,别写死在代码里。 symbol 传 NULL 就是当前品种;period 填 0 代表当前图表周期,也可以塞 ENUM_TIMEFRAMES 里的具体值。cmo_period 是算 Chande 动量振荡器的柱数,ema_period 是平滑因子的 EMA 周期,ma_shift 控制指标线相对价格图的横向偏移,applied_price 除了标准价格常数外还能接另一个指标句柄。 句柄不用了一定调 IndicatorRelease() 释放,不然 EA 跑久了内存会被吃住。下面这段声明里 InpPeriodCMO 默认 9,是 CMO 周期的常见起点,实盘里可先按 9 加载,再观察贵金属或外汇品种在 15 分钟图上的贴合度,调参注意杠杆品种的高风险。
class="type">int iVIDyA( class="type">class="kw">string symbol, class=class="str">"cmt">// symbol name ENUM_TIMEFRAMES period, class=class="str">"cmt">// period class="type">int cmo_period, class=class="str">"cmt">// Chande Momentum period class="type">int ema_period, class=class="str">"cmt">// smoothing factor period class="type">int ma_shift, class=class="str">"cmt">// horizontal shift of the indicator ENUM_APPLIED_PRICE applied_price class=class="str">"cmt">// price type or handle ); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestTrendVIDYA.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">//--- class="kw">input parameters class="kw">input class="type">uint InpPeriodCMO= class="num">9; class=class="str">"cmt">/* CMO Period */
◍ VIDYA 测试脚本里的状态枚举与全局变量
在 MT5 里跑 VIDYA 趋势测试,先得把指标句柄和周期参数接住。下面这段声明把偏移量、EMA 周期、应用价格三个输入项亮出来:InpShift 默认 0,InpPeriodEMA 默认 12,价格源用 PRICE_CLOSE。改 InpPeriodEMA 到 20 或 30,曲线平滑度会明显变化,EURUSD 的 M15 上回看能少掉不少假突破。 全局变量区里 handle 初始化为 INVALID_HANDLE,period_cmo 和 period_ema 先置 0,ind_digits 管小数位,ind_title 存描述文本。这些不是摆设——handle 拿不到合法值,后面所有 iVIDYA 调用都会返回空数组。 真正决定面板怎么画线的,是 ENUM_LINE_STATE 这个枚举。它把线态拆成 14 种:从 NONE、UP、DOWN 到 TURN_UP、CROSS_DOWN、TOUCH_ABOVE 都有。实盘里你只要盯 LINE_STATE_CROSS_UP 和 LINE_STATE_TURN_DOWN 两种,黄金 XAUUSD 在 H1 上出现 TURN_DOWN 后,后续 20 根 K 线内延续回落的概率偏高,但外汇贵金属杠杆高,信号失效也常见。 代码里还挂了 #include <Dashboard\Dashboard.mqh>,说明状态最终是抛给仪表盘渲染的。你开 MT5 新建脚本把这段粘进去,编译后能在可视化面板直接看 VIDYA 在哪几种态之间跳。
class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* VIDYA Shift */ class="kw">input class="type">uint InpPeriodEMA= class="num">12; class=class="str">"cmt">/* EMA Period */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_cmo=class="num">0; class=class="str">"cmt">// CMO calculation period class="type">int period_ema=class="num">0; class=class="str">"cmt">// EMA 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">//| TestTrendVIDYA.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 };
VIDYA 指标的输入参数与全局句柄
在 MT5 里写一套基于 CMO 调节平滑系数的 VIDYA 指标,先把外部可调参数摊开。CMO 周期默认 9、EMA 周期默认 12、价格取收盘价、偏移量 0,这几个数直接决定指标对节奏的敏感度。 下方全局变量负责接住指标句柄与计算周期:handle 存 iCustom 返回的 INVALID_HANDLE 初始值,period_cmo / period_ema 先置 0,等 OnInit 里读入输入参数再赋值;ind_digits 管小数位,ind_title 是指标说明字符串。 面板交互部分另开两个变量:mouse_bar_index 记录鼠标悬停取的 K 线序号,CDashboard *panel 指向 NULL,后续由 new 出来的面板对象接管。外汇与贵金属波动大、杠杆高,这类自定义指标仅作辅助,信号失真概率不低,实盘前务必在策略测试器跑一轮。
class="kw">input class="type">uint InpPeriodCMO= class="num">9; class=class="str">"cmt">/* CMO Period */ class="kw">input class="type">int InpShift = class="num">0; class=class="str">"cmt">/* VIDYA Shift */ class="kw">input class="type">uint InpPeriodEMA= class="num">12; class=class="str">"cmt">/* EMA Period */ class="kw">input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; class=class="str">"cmt">/* Applied Price */ class=class="str">"cmt">//--- global variables class="type">int handle=INVALID_HANDLE; class=class="str">"cmt">// Indicator handle class="type">int period_cmo=class="num">0; class=class="str">"cmt">// CMO calculation period class="type">int period_ema=class="num">0; class=class="str">"cmt">// EMA 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 里就不用反复计算,能少踩不少坑。 代码里对 CMO 和 EMA 周期做了下限保护:输入小于 1 时分别回退到 9 和 12。这意味着你在 EA 参数面板里误填 0,实际加载的仍是 VIDYA(9,12),回测和实盘初始状态会保持一致。 若策略带仪表板,OnInit 还要 new 一个 CDashboard 对象,坐标写死为 (20,20) 到宽 197、高 225。面板创建失败直接返回 INIT_FAILED,MT5 会拒绝加载 EA,避免半残状态跑单。外汇与贵金属波动剧烈,加载失败早发现比盘中崩面板安全得多。 下面这段是 OnInit 的核心骨架,含指标句柄与面板两支。逐行看:EventSetTimer(60) 设 60 秒定时器;period_cmo/period_ema 做三元下限;iVIDyA 拿句柄失败就打印错误码并返回 INIT_FAILED;面板部分 new CDashboard 后判空,设字体 Calibri 9 号。
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_cmo=class="type">int(InpPeriodCMO<class="num">1 ? class="num">9 : InpPeriodCMO); period_ema=class="type">int(InpPeriodEMA<class="num">1 ? class="num">12 : InpPeriodEMA); class=class="str">"cmt">//--- Set the indicator name and the number of decimal places ind_title=StringFormat("VIDYA(%lu,%lu)",period_cmo,period_ema); ind_digits=Digits()+class="num">1; class=class="str">"cmt">//--- Create indicator handle ResetLastError(); handle=iVIDyA(Symbol(),PERIOD_CURRENT,period_cmo,period_ema,InpShift,InpPrice); if(handle==INVALID_HANDLE) { PrintFormat("%s: Failed to create indicator handle %s. Error %ld",__FUNCTION__,ind_title,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Dashboard class=class="str">"cmt">//--- Create the panel panel=new CDashboard(class="num">1,class="num">20,class="num">20,class="num">197,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 class="kw">return(INIT_SUCCEEDED); }
◍ 在面板上排布双表格并绑定光标K线
这段初始化代码把行情面板搭成上下两块:ID 0 放单根 K 线数据,ID 1 放指标数据,两者都画在同一张面板背景上。
先看 ID 0 的表格:panel.CreateNewTable(0) 建表后,panel.DrawGrid(0,2,20,6,2,18,96) 把它从 Y=20 起画,6 行 2 列,单元格宽 18 高 96 像素。
ID 1 的 Y 坐标不是写死的:int y1=panel.TableY2(0)+22 先取 ID 0 表格底边,再下移 22 像素作为起点,避免两块表格重叠。随后 panel.DrawGrid(1,2,y1,3,2,18,96) 画出 3 行 2 列的指示表。
mouse_bar_index=0 把鼠标对应 K 线预设为最新一根,DrawData(0,TimeCurrent()) 立即把当前 bar 数据刷到面板。外汇与贵金属波动剧烈,面板数值仅作实时参考,实际下单仍需结合风控。
最后 return(INIT_SUCCEEDED) 表示初始化通过,EA 可继续跑。复制这段代码进 MT5 的 OnInit,能直接验证面板布局是否符合你的盯盘习惯。
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">96); 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">96); 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 返回 false 时说明释放失败,用 GetLastError 把错误码打出来,方便你直接在 MT5 终端日志里定位。 //+------------------------------------------------------------------+
| // | 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(""); } 如果 EA 里自己画了仪表板(比如用 CPanel 派生的对象),OnDeinit 里还要判空再 delete。panel 不为 NULL 才删,避免对野指针做释放导致 MT5 崩溃。 //+------------------------------------------------------------------+
| // | 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; } 实盘加载这类 EA 前,建议你在策略测试器里反复 attach / remove 二十次,观察终端是否出现 handle 未释放的警告。外汇与贵金属杠杆高,EA 异常退出可能让你错过大段行情,清理逻辑写稳是基本功。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- Release handle of the indicator ResetLastError(); if(!IndicatorRelease(handle)) PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError()); class=class="str">"cmt">//--- Clear all comments on the chart Comment(""); class=class="str">"cmt">//--- If the panel object exists, class="kw">delete it if(panel!=NULL) class="kw">delete panel; }
「从指标句柄抓数与判线状态」
做盘面仪表板时,所有指标线和状态都不是直接读缓冲,而是通过句柄在 OnChartEvent() 里响应光标所在柱索引后取数。编译挂上 EA 后,面板就能实时刷出当前柱的指标值与走向,外汇和贵金属品种波动快,这种随光标联动的读法比定时轮询更跟手。 IndicatorValue() 只干一件事:用 CopyBuffer 从指定句柄、缓冲号、偏移索引拷 1 个值到长度为 1 的数组。失败就 PrintFormat 报错并返回 EMPTY_VALUE,成功回 array[0],调用前 ResetLastError 是必须的,否则上一个错误码会污染判断。 LineState() 连续取 index、index+1、index+2 三根柱的值,用 NormalizeDouble 按指标小数位对齐后再比大小。它把线分成转上、上行、上停、转下、下行、下停六态,任一值为空直接回 LINE_STATE_NONE——这意味着你面板显示「无状态」时,先查句柄是不是没拿到缓冲而不是逻辑写错。 附件里的 TestTrendVIDYA.mq5 是可跑的验证样本,开 MT5 加载后移动鼠标就能看到六态切换,调 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class="type">class="kw">double array[class="num">1]={class="num">0}; ResetLastError(); if(CopyBuffer(ind_handle,buffer_num,index,class="num">1,array)!=class="num">1) { PrintFormat("%s: CopyBuffer failed. Error %ld",__FUNCTION__,GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the state of the indicator line | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_LINE_STATE LineState(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1,class="num">2) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class="kw">const class="type">class="kw">double value2=IndicatorValue(ind_handle,index+class="num">2,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE || value2==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Line upward reversal(value2>value1 && value0>value1) if(NormalizeDouble(value2-value1,ind_digits)>class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_TURN_UP; class=class="str">"cmt">//--- Line upward direction(value2<=value1 && value0>value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)>class="num">0) class="kw">return LINE_STATE_UP; class=class="str">"cmt">//--- Line upward stop(value2<=value1 && value0==value1) else if(NormalizeDouble(value2-value1,ind_digits)<=class="num">0 && NormalizeDouble(value0-value1,ind_digits)==class="num">0) class="kw">return LINE_STATE_STOP_UP; class=class="str">"cmt">//--- Line downward reversal(value2<value1 && value0<value1) if(NormalizeDouble(value2-value1,ind_digits)<class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_TURN_DOWN; class=class="str">"cmt">//--- Line downward direction(value2>=value1 && value0<value1) else if(NormalizeDouble(value2-value1,ind_digits)>=class="num">0 && NormalizeDouble(value0-value1,ind_digits)<class="num">0) class="kw">return LINE_STATE_DOWN; class=class="str">"cmt">//--- Line downward stop(value2>=value1 && value0==value1)
◍ 指标线与参考位的相对状态判定
这段逻辑用于把某根指标线在相邻两根 K 线上的取值,与指定参考位做比较,归成可枚举的状态。它同时支持单参考位(level0)和双参考位(level0、level1),当 level1 传入 EMPTY_VALUE 时自动退化为只比较 level0。 函数先取 index 与 index+1 两处的缓冲值 value0、value1,任意一处为 EMPTY_VALUE 直接返回 LINE_STATE_NONE,避免脏数据污染后续判断。随后用 NormalizeDouble 按 ind_digits 精度对齐差值,再依次判别低于、高于、上穿、下穿、从下触碰、从上触碰、等值等七类状态。 注意一处疑似笔误:从上方触碰参考位的分支(value1>level0 且 value0==level0)返回的是 LINE_STATE_TOUCH_BELOW,而非语义上应有的 LINE_STATE_TOUCH_ABOVE。在 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(class="kw">const class="type">int ind_handle,class="kw">const class="type">int index,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">double level0,class="kw">const class="type">class="kw">double level1=EMPTY_VALUE) { class=class="str">"cmt">//--- Get the values of the indicator line with the shift(class="num">0,class="num">1) relative to the passed index class="kw">const class="type">class="kw">double value0=IndicatorValue(ind_handle,index, buffer_num); class="kw">const class="type">class="kw">double value1=IndicatorValue(ind_handle,index+class="num">1,buffer_num); class=class="str">"cmt">//--- If at least one of the values could not be obtained, class="kw">return an undefined value if(value0==EMPTY_VALUE || value1==EMPTY_VALUE) class="kw">return LINE_STATE_NONE; class=class="str">"cmt">//--- Define the second level to compare class="type">class="kw">double level=(level1==EMPTY_VALUE ? level0 : level1); class=class="str">"cmt">//--- The line is below the level(value1<level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_UNDER; class=class="str">"cmt">//--- The line is above the level(value1>level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_ABOVE; class=class="str">"cmt">//--- The line crossed the level upwards(value1<=level && value0>level0) if(NormalizeDouble(value1-level,ind_digits)<=class="num">0 && NormalizeDouble(value0-level0,ind_digits)>class="num">0) class="kw">return LINE_STATE_CROSS_UP; class=class="str">"cmt">//--- The line crossed the level downwards(value1>=level && value0<level0) if(NormalizeDouble(value1-level,ind_digits)>=class="num">0 && NormalizeDouble(value0-level0,ind_digits)<class="num">0) class="kw">return LINE_STATE_CROSS_DOWN; class=class="str">"cmt">//--- The line touched the level from below(value1<level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)<class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- The line touched the level from above(value1>level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)>class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_TOUCH_BELOW; class=class="str">"cmt">//--- Line is equal to the level value(value1==level0 && value0==level0) if(NormalizeDouble(value1-level,ind_digits)==class="num">0 && NormalizeDouble(value0-level0,ind_digits)==class="num">0) class="kw">return LINE_STATE_EQUALS; class=class="str">"cmt">//--- Undefined state class="kw">return LINE_STATE_NONE; }
把线条状态映射成可读字符
在 MT5 自定义指标里,价格线与参考线的相对关系常被编码成枚举 ENUM_LINE_STATE。直接把枚举值抛给面板,交易者看不懂,所以需要一层转换函数把机器状态翻成英文短词。 下面这个函数就是干这事的:传入一个 ENUM_LINE_STATE,用 switch 把 13 种状态分别返回对应字符串,比如 LINE_STATE_UP 返回 "Up",LINE_STATE_CROSS_DOWN 返回 "Crossing Down",未匹配到就返回 "Unknown"。
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW : class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE : class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } }
class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class=class="str">"cmt">//--- Declare the variables to receive data in them class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Exit if unable to get the current prices if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; class=class="str">"cmt">//--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class=class="str">"cmt">//--- Set font parameters for bar and indicator data headers class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); class=class="str">"cmt">//--- Set font parameters for bar and indicator data
class="type">class="kw">string LineStateDescription(class="kw">const ENUM_LINE_STATE state) { class="kw">switch(state) { case LINE_STATE_UP : class="kw">return "Up"; case LINE_STATE_STOP_UP : class="kw">return "Stop Up"; case LINE_STATE_TURN_UP : class="kw">return "Turn Up"; case LINE_STATE_DOWN : class="kw">return "Down"; case LINE_STATE_STOP_DOWN : class="kw">return "Stop Down"; case LINE_STATE_TURN_DOWN : class="kw">return "Turn Down"; case LINE_STATE_ABOVE : class="kw">return "Above level"; case LINE_STATE_UNDER : class="kw">return "Under level"; case LINE_STATE_CROSS_UP : class="kw">return "Crossing Up"; case LINE_STATE_CROSS_DOWN : class="kw">return "Crossing Down"; case LINE_STATE_TOUCH_BELOW : class="kw">return "Touch from Below"; case LINE_STATE_TOUCH_ABOVE : class="kw">return "Touch from Above"; case LINE_STATE_EQUALS : class="kw">return "Equals"; class="kw">default : class="kw">return "Unknown"; } } class="type">void DrawData(class="kw">const class="type">int index,class="kw">const class="type">class="kw">datetime time) { class="type">MqlTick tick={class="num">0}; class="type">MqlRates rates[class="num">1]; if(!SymbolInfoTick(Symbol(),tick)) class="kw">return; if(CopyRates(Symbol(),PERIOD_CURRENT,index,class="num">1,rates)!=class="num">1) class="kw">return; class="type">int size=class="num">0; class="type">uint flags=class="num">0; class="type">uint angle=class="num">0; class="type">class="kw">string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,class="num">9,FW_BOLD); panel.DrawText("Bar data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">0)-class="num">16,clrMaroon,panel.Width()-class="num">6); panel.DrawText("Indicator data ["+(class="type">class="kw">string)index+"]",class="num">3,panel.TableY1(class="num">1)-class="num">16,clrGreen,panel.Width()-class="num">6); }
「把 K 线和指标值塞进面板表格」
在 MT5 自建面板里把当前柱的数据铺出来,核心是先定字体再逐格写字。下面这段逻辑把第 0 根 bar 的日期、时间、OHLC 分别写进表 0 的第 0~5 行,指标标题和数值交给表 1,肉眼盯盘时不用切窗口就能比对。 字体用 9 号是折中:再小在 4K 屏也发虚,再大一行就挤掉两根 bar 的显示空间。CellX/CellY 的三个参数依次是表号、行号、列号,末尾 +2 是为了避开单元格边框,不然字会贴边被裁掉。 指标值取的是 shift=0 的实时柱,也就是说面板上的数字会和主图最新 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线塞进面板
这段逻辑干的事很直接:鼠标在图上动一下,就把对应那根K线的指标数值和线状态刷到自定义面板里。先处理数值显示,若指标值不是 EMPTY_VALUE 就按 ind_digits 精度转字符串,否则留空,避免面板出现乱码或野指针文本。 LineState 取回当前线状态枚举,再用 LineStateDescription 翻成可读文字,DrawText 的坐标都靠 CellX/CellY 算出来,偏移 +2 像素防止贴边。每次画完调 ChartRedraw 强制重绘,否则 MT5 可能攒着不刷,肉眼看到的就是面板滞后一两帧。 OnChartEvent 里只认两种触发:CHARTEVENT_MOUSE_MOVE 和 CHARTEVENT_CLICK。用 ChartXYToTimePrice 把鼠标像素坐标反推成时间+价格,再 iBarShift 换成当前品种当前周期的 bar 索引,写进全局变量 mouse_bar_index 后丢给 DrawData 画。 自定义事件 id 大于 CHARTEVENT_CUSTOM 时,目前只 PrintFormat 打日志,留了个口子给以后接面板关闭按钮。外汇和贵金属行情跳空多,iBarShift 在跨休市时段可能返回 -1,DrawData 里最好自己挡一下,否则容易在面板上画出越界数据。
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 panel.DrawText("Line state", panel.CellX(class="num">1,class="num">1,class="num">0)+class="num">2, panel.CellY(class="num">1,class="num">1,class="num">0)+class="num">2); ENUM_LINE_STATE state=LineState(handle,index,class="num">0); panel.DrawText(LineStateDescription(state),panel.CellX(class="num">1,class="num">1,class="num">1)+class="num">2,panel.CellY(class="num">1,class="num">1,class="num">1)+class="num">2,clrNONE,class="num">90); class=class="str">"cmt">//--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- Handling the panel class=class="str">"cmt">//--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { class=class="str">"cmt">//--- Declare the variables to record time and price coordinates in them class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); class=class="str">"cmt">//--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } class=class="str">"cmt">//--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { class=class="str">"cmt">//--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id(lparam): %lu, event message(sparam): %s",__FUNCTION__,id,lparam,sparam); } }
把这条线请下神坛
这套趋势指标接入 EA 的模板走到这里,把 MT5 里能直接调用的 13 个趋势类指标都过了一遍,从 ADX、AMA 到 Ichimoku、VIDYA,附件里每个测试 EA 都在 31–36 KB 区间,Dashboard.mqh 单独 217.85 KB 管多品种面板。多周期、多交易品种指标怎么稳稳喂给 EA 这块还没铺开,只能等下一篇专门工具。 你现在就能开 MT5 把 TestTrendSAR.mq5 这类文件原样丢进编辑器编译,跑一遍策略测试器看信号触发节奏;外汇和贵金属杠杆高,回测顺不代表实盘能复刻,信号延迟和滑点都可能吃掉预期。 代码随便拷去用可以,但权利还在原作者手里,部分转载明令禁止。线画得再漂亮也只是概率参考,别拿它当圣杯。