MQL5 Cookbook: 处理典型图表事件·进阶篇
◍ EA 如何接管按键、鼠标与图表事件
MT5 的 EA 不是只能盯 K 线,它能直接吃进终端的各类交互事件。按键分支里,lparam 捕获的是按键代码:上箭头是 38、下箭头是 40,分别对应 TryToBuy() 和 TryToSell(),手数止损止盈走 InpLot、InpStopLoss 这类外部输入。 鼠标移动事件默认不触发,得在初始化里用 ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, is_mouse) 开权限。光标 X/Y 落在 lparam 和 dparam,利用 InpChartShiftSize 控制零柱线到右框的偏移百分比,滑过边界弹买卖确认窗——这套在 EventProcessor2 里可复现。 图形对象的新建、改属性、删除都各有一套开关:CHART_EVENT_OBJECT_CREATE / OBJECT_DELETE 等。删对象有个坑,属性在被删后读不到,想判断类型得在删除前自行缓存。开发者若肯加图表历史快照会更省事。 点击类事件最容易误触发。在对象上点一下,往往同时抛出「对象点击」和「图表点击」两个事件,导致买箭头被重复画。用全局变量 gLastTime 卡 250ms 间隔忽略二次调用,EventProcessor7 就是这么收口的。 文本框编辑事件(CHARTEVENT_OBJECT_ENDEDIT)适合做指令入口:输入 buy/sell 不区分大小写,EA 下单并把框染蓝或红。图表修改事件则能锁设置,有人改栅格或背景色时 EA 直接回写旧值。外汇与贵金属波动剧烈,这类半自动交互仍属高风险操作,参数请先在模拟盘验证。
class=class="str">"cmt">//--- class="num">1 case CHARTEVENT_KEYDOWN: { class=class="str">"cmt">//--- "up" arrow if(lparam==class="num">38) TryToBuy(); class=class="str">"cmt">//--- "down" arrow else if(lparam==class="num">40) TryToSell(); comment+="class="num">1) keystroke"; class=class="str">"cmt">//--- break; } class=class="str">"cmt">//--- mouse move class="type">bool is_mouse=false; if(InpIsEventMouseMove) is_mouse=true; ChartSetInteger(class="num">0,CHART_EVENT_MOUSE_MOVE,is_mouse); class=class="str">"cmt">//--- class="num">2 case CHARTEVENT_MOUSE_MOVE: { comment+="class="num">2) mouse"; class=class="str">"cmt">//--- if a mouse event is processed
鼠标越界弹窗下单与对象创建事件
在 MT5 图表脚本里,可以通过捕捉鼠标移动事件来实现半自动下单触发。下面这段逻辑先判断开关 InpIsEventMouseMove,开启后调用 ChartShiftSet(true) 把图表平移打开,再用 ChartShiftSizeSet 按百分比设定右侧留白区。 核心在于算出卖单边界的像素坐标:chart_shift_x = 图表总宽 - 总宽 * InpChartShiftSize / 100。当鼠标 X 从左侧跨过这条边界(lparam 大于边界且上一帧 last_mouse_x 小于边界),就弹 MessageBox 问「Yes 买 / No 卖」,分别进 TryToBuy / TryToSell。外汇与贵金属波动剧烈,这种点击触发不带任何风控,实盘误触可能造成直接敞口,只建议在模拟盘验证。 另一个常被忽略的开关是 CHART_EVENT_OBJECT_CREATE。把 InpIsEventObjectCreate 置真后,ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true) 会让脚本收到画对象事件;在 case CHARTEVENT_OBJECT_CREATE 里可用 ObjectsTotal(0,0,OBJ_HLINE) 统计当前水平线数量,方便你做「画一根线就记录一次」的辅助逻辑。 别把弹窗当信号源:它只认边界穿越这一动作,不分辨趋势和流动性。真要接小布盯盘的结论,得把 TryToBuy/Sell 里换成你的置信度过滤。
if(InpIsEventMouseMove) { class="type">long class="kw">static last_mouse_x; class=class="str">"cmt">//--- enable shift if(ChartShiftSet(true)) class=class="str">"cmt">//--- set shift size if(ChartShiftSizeSet(InpChartShiftSize)) { class=class="str">"cmt">//--- chart width class="type">int chart_width=ChartWidthInPixels(); class=class="str">"cmt">//--- calculate X coordinate of shift border class="type">int chart_shift_x=(class="type">int)(chart_width-chart_width*InpChartShiftSize/class="num">100.); class=class="str">"cmt">//--- border crossing condition if(lparam>chart_shift_x && last_mouse_x<chart_shift_x) { class="type">int res=MessageBox("Yes: buy / No: sell","Trade operation",MB_YESNO); class=class="str">"cmt">//--- buy if(res==IDYES) TryToBuy(); class=class="str">"cmt">//--- sell else if(res==IDNO) TryToSell(); } class=class="str">"cmt">//--- store mouse X coordinate last_mouse_x=lparam; } } class=class="str">"cmt">//--- break; } class=class="str">"cmt">//--- object create class="type">bool is_obj_create=false; if(InpIsEventObjectCreate) is_obj_create=true; ChartSetInteger(class="num">0,CHART_EVENT_OBJECT_CREATE,is_obj_create); class=class="str">"cmt">//--- class="num">3 case CHARTEVENT_OBJECT_CREATE: { comment+="class="num">3) create graphical object"; class=class="str">"cmt">//--- if graphical object creation event is processed if(InpIsEventObjectCreate) { class=class="str">"cmt">//--- capture creation of horizontal line class="type">int all_hor_lines=ObjectsTotal(class="num">0,class="num">0,OBJ_HLINE);
「单线模式下取可视区间高低中三价」
当水平线总数被限定为 1 条时,脚本会直接进入单线分支,把传入的参数名作为主线对象名,并基于当前图表可视 K 线重算锚定价格。这里用 ChartVisibleBars() 拿到可视柱数,再减 1 作为 CopyHigh / CopyLow 的复制数量,意味着只处理视窗内完整可见的行情,不碰右侧未收线部分。 复制高点与低点都做了返回值校验:若 copied 不等于 visible_bars_num-1,就打印失败并 return,避免后续用空数组算出离谱价位。这一道防御在 MT5 切换周期或图表未完全渲染时尤其关键,否则 ArrayMaximum 可能落在无效数据上。 真正定价的三行是核心:ch_high_pr 取 highs 最大值并按 _Digits 规范化,ch_low_pr 取 lows 最小值规范化,ch_mid_pr 则是二者均值。若主线对象已存在就用 ObjectMove 挪到 high 价;失败同样打印并返回。随后另起一个名为 Hor_line_min 的字符串准备挂 low 线——外汇与贵金属波动大,这类自动重锚线只作参考,实盘仍需警惕滑点风险。
if(all_hor_lines==class="num">1) { class="type">class="kw">string hor_line_name1=sparam; class=class="str">"cmt">//--- calculate levels class="type">int visible_bars_num=ChartVisibleBars(); class=class="str">"cmt">//--- arrays for high and low prices class="type">class="kw">double highs[],lows[]; class=class="str">"cmt">//--- class="type">int copied=CopyHigh(_Symbol,_Period,class="num">0,visible_bars_num-class="num">1,highs); if(copied!=visible_bars_num-class="num">1) { Print("Failed to copy highs!"); class="kw">return; } copied=CopyLow(_Symbol,_Period,class="num">0,visible_bars_num-class="num">1,lows); if(copied!=visible_bars_num-class="num">1) { Print("Failed to copy lows!"); class="kw">return; } class=class="str">"cmt">//--- high and low prices class="type">class="kw">double ch_high_pr,ch_low_pr,ch_mid_pr; class=class="str">"cmt">//--- ch_high_pr=NormalizeDouble(highs[ArrayMaximum(highs)],_Digits); ch_low_pr=NormalizeDouble(lows[ArrayMinimum(lows)],_Digits); ch_mid_pr=NormalizeDouble((ch_high_pr+ch_low_pr)/class="num">2.,_Digits); class=class="str">"cmt">//--- place created line on high if(ObjectFind(class="num">0,hor_line_name1)>-class="num">1) if(!ObjectMove(class="num">0,hor_line_name1,class="num">0,class="num">0,ch_high_pr)) { Print("Failed to move!"); class="kw">return; } class=class="str">"cmt">//--- create line on low class="type">class="kw">string hor_line_name2="Hor_line_min";
◍ 用事件同步图表对象的颜色
在 MT5 的图表事件处理里,CHARTEVENT_OBJECT_CHANGE 会在你通过属性框改了某个对象后触发。下面这段逻辑做的是:抓住当前被改的对象名,把它的最新颜色同步给图上其余所有对象,适合做一套联动的高亮水平线组。 先拿 sparam 拿到改动对象的名字,用 ObjectFind 确认它确实在图上(返回值大于 -1 才继续)。随后用 ObjectGetInteger 取 OBJPROP_COLOR,再用 ObjectsTotal 数出当前图表总对象数。 接着一个 for 循环从 0 扫到 all_other_objects-1,用 ObjectName 拿每个对象的名字;只要名字和当前改的不是同一个,就 ObjectSetInteger 把颜色刷成一样的。任意一次设置失败会 Print 报错并 return,外汇和贵金属图表对象多的时候这种失败概率会随对象数上升,属正常现象。 别把联动当默认 MT5 不会自动帮你同步对象属性,上面这套得自己写进 OnChartEvent;不改代码,你拖一根线改色,别的线依旧各走各的。
case CHARTEVENT_OBJECT_CHANGE: { comment+="class="num">4) change object properties via properties dialog"; class=class="str">"cmt">//--- class="type">class="kw">string curr_obj_name=sparam; class=class="str">"cmt">//--- find the changed object if(ObjectFind(class="num">0,curr_obj_name)>-class="num">1) { class=class="str">"cmt">//--- get object class="type">color class="type">color curr_obj_color=(class="type">color)ObjectGetInteger(class="num">0,curr_obj_name,OBJPROP_COLOR); class=class="str">"cmt">//--- total number of objects on chart class="type">int all_other_objects=ObjectsTotal(class="num">0); class=class="str">"cmt">//--- find other objects for(class="type">int obj_idx=class="num">0;obj_idx<all_other_objects;obj_idx++) { class="type">class="kw">string other_obj_name=ObjectName(class="num">0,obj_idx); if(StringCompare(curr_obj_name,other_obj_name)!=class="num">0) if(!ObjectSetInteger(class="num">0,other_obj_name,OBJPROP_COLOR,curr_obj_color)) { Print("Failed to change the object class="type">color!"); class="kw">return; } } } break; }
图表事件里的对象清理与鼠标落点建标
在图表事件处理里,删除对象的开关由输入参数 InpIsEventObjectDelete 控制。代码通过 ChartSetInteger(0,CHART_EVENT_OBJECT_DELETE,is_obj_delete) 把开关写到当前图表,true 时图表才会派发对象删除事件,避免误删手动画的线。 一段 EURUSD.e 的 W1 日志很说明问题:移除前垂直线数量是 4,移除后变成 0,说明事件触发后清理逻辑确实把 4 条线全摘掉了。外汇和贵金属品种波动大,这种批量清理若开关误开,可能把你自己留的分析线一起删掉,高风险操作前先确认参数。 鼠标点击分支里,CHARTEVENT_CLICK 会把 lparam、dparam 当作像素坐标。用 ChartXYToTimePrice 转成时间 obj_time 和价格 obj_price,再 ObjectCreate 画 OBJ_ARROW_BUY 箭头,名称按 buy_sign_ 加静态计数器递增,点一次多一个买标。 别把坐标转换当理所当然。ChartXYToTimePrice 失败时就别建对象,否则箭头会落到 (0,0) 附近变成垃圾图形;实盘前在 MT5 用 EURUSD 的 M1 连点几下,看箭头是否精准落在你点的 K 线和价位上。
class=class="str">"cmt">//--- redraw chart ChartRedraw(); } class=class="str">"cmt">//--- break; class=class="str">"cmt">//--- object class="kw">delete class="type">bool is_obj_delete=false; if(InpIsEventObjectDelete) is_obj_delete=true; ChartSetInteger(class="num">0,CHART_EVENT_OBJECT_DELETE,is_obj_delete); class=class="str">"cmt">//--- class="num">6 case CHARTEVENT_CLICK: { comment+="class="num">6) mouse click on chart"; class=class="str">"cmt">//--- object counter class="kw">static class="type">uint sign_obj_cnt; class="type">class="kw">string buy_sign_name="buy_sign_"+IntegerToString(sign_obj_cnt+class="num">1); class=class="str">"cmt">//--- coordinates class="type">int mouse_x=(class="type">int)lparam; class="type">int mouse_y=(class="type">int)dparam; class=class="str">"cmt">//--- time and price class="type">class="kw">datetime obj_time; class="type">class="kw">double obj_price; class="type">int sub_window; class=class="str">"cmt">//--- convert the X and Y coordinates to the time and price values if(ChartXYToTimePrice(class="num">0,mouse_x,mouse_y,sub_window,obj_time,obj_price)) { class=class="str">"cmt">//--- create object if(!ObjectCreate(class="num">0,buy_sign_name,OBJ_ARROW_BUY,class="num">0,obj_time,obj_price)) { Print("Failed to create buy sign!"); class="kw">return; } class=class="str">"cmt">//--- redraw chart ChartRedraw(); class=class="str">"cmt">//--- increase object counter sign_obj_cnt++; } class=class="str">"cmt">//--- break; } class=class="str">"cmt">//--- class="num">7 case CHARTEVENT_OBJECT_CLICK: {
「用鼠标点击在图上落卖出箭头」
图表事件里捕捉到鼠标点击图形对象时,先把被点对象名取出来尝试删除,成功就重绘并准备画反向信号。下面这段逻辑把点击坐标转成时间价格再建卖出箭头,是 MT5 里手动标信号最常抄的写法。 代码里用 ObjectDelete(0,sign_name) 删掉原买入箭头,ChartRedraw() 立即刷新。接着用静态计数器拼出 sell_sign_ 加序号的名字,避免重名覆盖。 鼠标 X/Y 来自 lparam、dparam 强转 int,交给 ChartXYToTimePrice() 换算成 obj_time 与 obj_price;换算成功才 ObjectCreate 画 OBJ_ARROW_SELL。外汇和贵金属波动快,手动标点仅作参考,实际下单仍属高风险行为,信号箭头不代表行情必然反转。 另有一段用 GetTickCount 算间隔:若距上次超过 250 毫秒才处理点击,能过滤掉连点造成的重复触发。拖拽事件 CHARTEVENT_OBJECT_DRAG 中若拖的是两条时间限制线,就进后续分支——这部分留给你在 MT5 里挂 EA 跑一遍看 sparam 返回什么。
comment+="class="num">7) mouse click on graphical object"; class=class="str">"cmt">//--- class="type">class="kw">string sign_name=sparam; class=class="str">"cmt">//--- class="kw">delete buy arrow if(ObjectDelete(class="num">0,sign_name)) { class=class="str">"cmt">//--- redraw chart ChartRedraw(); class=class="str">"cmt">//--- class="kw">static class="type">uint sign_obj_cnt; class="type">class="kw">string sell_sign_name="sell_sign_"+IntegerToString(sign_obj_cnt+class="num">1); class=class="str">"cmt">//--- coordinates class="type">int mouse_x=(class="type">int)lparam; class="type">int mouse_y=(class="type">int)dparam; class=class="str">"cmt">//--- time and price class="type">class="kw">datetime obj_time; class="type">class="kw">double obj_price; class="type">int sub_window; class=class="str">"cmt">//--- convert the X and Y coordinates to the time and price values if(ChartXYToTimePrice(class="num">0,mouse_x,mouse_y,sub_window,obj_time,obj_price)) { class=class="str">"cmt">//--- create object if(!ObjectCreate(class="num">0,sell_sign_name,OBJ_ARROW_SELL,class="num">0,obj_time,obj_price)) { Print("Failed to create sell sign!"); class="kw">return; } class=class="str">"cmt">//--- redraw chart ChartRedraw(); class=class="str">"cmt">//--- increase object counter sign_obj_cnt++; } } class=class="str">"cmt">//--- break; } class=class="str">"cmt">//--- store the moment of creation gLastTime=GetTickCount(); class="type">uint lastTime=GetTickCount(); if((lastTime-gLastTime)>class="num">250) { class=class="str">"cmt">//--- click handling } class=class="str">"cmt">//--- class="num">8 case CHARTEVENT_OBJECT_DRAG: { comment+="class="num">8) move graphical object with mouse"; class="type">class="kw">string curr_obj_name=sparam; class=class="str">"cmt">//--- if one of the vertical lines is moved if(!StringCompare(curr_obj_name,gTimeLimit1_name) || !StringCompare(curr_obj_name,gTimeLimit2_name)) {