构建自动运行的 EA(第 05 部分):手工触发器(II)·综合运用
(3/3)·从重复代码里抽出止盈止损线逻辑,让手工 EA 和自动 EA 共用同一套底层而不互相拖垮
「把止盈止损线塞进鼠标类里」
在 MT5 里做交互式挂单辅助线,最省事的位置不是 EA 的 OnChartEvent,而是 C_Mouse 类自身的事件处理程序。鼠标移开图表,示意线跟着消失,不用额外写清理逻辑,这是把 TP/SL 横线托管给鼠标类最直接的收益。 原本的宏定义要重做。旧的单一名字宏删掉,换成带前缀的 MOUSE_T 和 MOUSE_S,分别代表止盈、止损横线;再补两个颜色变量存线色,创建时只需传名字和颜色,代码量一下砍掉大半。 构造函数现在要比以前多收几个参数——主要是把财务意义上的止损止盈值直接转成点数存进变量,而不是等 EA 每次调用函数去算。直接读变量比调函数快,鼠标移动事件高频触发时这点延迟差异肉眼可感。 析构侧只改负责删对象的函数:按前缀批量删,凡是名字以 MOUSE_ 开头的 HLINE 一并清除,以后扩展别的鼠标线也能复用。 关键交互在按键判断:SHIFT 或 CTRL 单独按住时才画 TP/SL 线,俩键同时按忽略。若线已存在先删掉,MT5 会立刻发对象删除事件,处理完再把价格线贴回图表;第一次创建要求数值大于 0,避免图表上漂着怪线,创建后打标记,后续移动只更新位置不重建。
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="type">uint BtnStatus; class="type">class="kw">double Price; class="kw">static class="type">class="kw">double mem = class="num">0; (*mouse).DispatchMessage(id, lparam, dparam, sparam); (*mouse).GetStatus(Price, BtnStatus); if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)) { if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04); } if (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus)) { class=class="str">"cmt">// This point ... if (def_BtnLeftClick(BtnStatus) && (mem == class="num">0)) (*manager).CreateOrder(def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, mem = Price, user03, user02, user01, user04); }else mem = class="num">0; } class="macro">#define def_PrefixNameObject "MOUSE_" class="macro">#define def_MouseLineName def_PrefixNameObject + "H" class="macro">#define def_MouseLineTake def_PrefixNameObject + "T" class="macro">#define def_MouseLineStop def_PrefixNameObject + "S" class="macro">#define def_MouseName "MOUSE_H" class="type">void CreateLineH(class="type">void) class="type">void CreateLineH(class="kw">const class="type">class="kw">string szName, class="kw">const class="type">color cor) { ObjectCreate(m_Infos.Id, def_MouseName, OBJ_HLINE, class="num">0, class="num">0, class="num">0);
把资金风险换算成鼠标跟随的价位线
这个类构造函数 C_Mouse 接收入场色、止盈色、止损色三组颜色,以及以金额表达的止损/止盈幅度(FinanceStop、FinanceTake)和杠杆倍数 Leverage,把「想亏多少美金」直接转成图表上能看的「离价多少点」。 核心两步在背景高亮行:m_Infos.PointsTake = FinanceToPoints(FinanceTake, Leverage) 与 m_Infos.PointsStop = FinanceToPoints(FinanceStop, Leverage)。也就是说,100 美金止损在 1:100 杠杆、标普类品种上可能被折成几十个点,具体数值取决于合约规模和当前报价,开 MT5 把 FinanceToPoints 的实现跑一遍就能看到自己品种的映射。 随后用 ChartSetInteger 打开 CHART_EVENT_MOUSE_MOVE 与 CHART_EVENT_OBJECT_DELETE,鼠标移动时实时重绘水平线;CreateLineH 按传入颜色建一条 OBJ_HLINE。外汇与贵金属波动剧烈,杠杆会放大点位换算误差,实盘前先用策略测试仪验证映射。
ObjectSetString(m_Infos.Id, def_MouseName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_BACK, class="kw">false); ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_COLOR, m_Infos.Cor); ObjectCreate(m_Infos.Id, szName, OBJ_HLINE, class="num">0, class="num">0, class="num">0); ObjectSetString(m_Infos.Id, szName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(m_Infos.Id, szName, OBJPROP_BACK, class="kw">false); ObjectSetInteger(m_Infos.Id, szName, OBJPROP_COLOR, cor); } C_Mouse(class="kw">const class="type">color corPrice, class="kw">const class="type">color corTake, class="kw">const class="type">color corStop, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">uint Leverage) { m_Infos.Id = ChartID(); m_Infos.CorPrice = corPrice; m_Infos.CorTake = corTake; m_Infos.CorStop = corStop; m_Infos.PointsTake= FinanceToPoints(FinanceTake, Leverage); m_Infos.PointsStop= FinanceToPoints(FinanceStop, Leverage); ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true); CreateLineH(def_MouseLineName, m_Infos.CorPrice); }
◍ 鼠标交互下的对象回收与重绘
在 MT5 自定义指标里用鼠标拖线做价格参考时,对象被手动删除会触发 CHARTEVENT_OBJECT_DELETE,若不在析构和消息分发里接管,图表会留下残影或失去交互锚点。下面这段类方法展示了退出时关闭删除事件并清场,以及在对象被删后按名字重建水平线的做法。 析构函数 ~C_Mouse() 先关闭图表的删除事件广播,再用 ObjectsDeleteAll 按前缀清理一类对象,最后单独删掉鼠标主对象 def_MouseName,避免下次加载时 ID 冲突。DispatchMessage 里用 static bool bView 记录视图状态,对 CHARTEVENT_OBJECT_DELETE 判断 sparam:删的是鼠标名就重建默认水平线,删的是水平线名就按上次价格 m_Infos.CorPrice 重建,保证拖走的线被误删也能立刻回来。 实盘加载这类脚本后,你在 EURUSD 的 H1 上手动删掉参考线,大概率会看到它在原价格或记录价瞬间重生;外汇与贵金属杠杆高,这类辅助线只管参考、不参与下单风控。
~C_Mouse() { ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, class="kw">false); ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject); ObjectDelete(m_Infos.Id, def_MouseName); } class="type">void DispatchMessage(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="type">int w; class="type">class="kw">datetime dt; class="kw">static class="type">bool bView = class="kw">false; class="kw">switch (id) { case CHARTEVENT_OBJECT_DELETE: if (sparam == def_MouseName) CreateLineH(); if (sparam == def_MouseLineName) CreateLineH(def_MouseLineName, m_Infos.CorPrice); class="kw">break; case CHARTEVENT_MOUSE_MOVE:
「鼠标拖拽时止盈止损线的实时跟随」
在自定义指标或 EA 的图表事件回调里,用 ChartXYToTimePrice 把鼠标屏幕坐标转成时间与价格,是拖拽类辅助画线的第一步。下面这段逻辑展示了如何在按住 Ctrl 或 Shift 的不同时,让止盈、止损水平线跟着光标价格实时移动。 核心判断在 def_CTRL_Press 与 def_SHIFT_Press 返回值不同:两者互斥时才显示并移动辅助线,否则清掉。也就是说,同时按或都不按,bView 会被置 false 并 ObjectsDeleteAll 删前缀对象,画面回到干净状态。 移动时以 m_Infos.Price 为基准,PointsTake 为正则上方画止盈、PointsStop 为正则下方画止损;Shift 按下时方向反转(止盈向下、止损向上),这是给反向挂单留的快捷操作。外汇与贵金属杠杆高,这类辅助线只作参考,实际成交价可能因滑点偏离。 把下面代码粘进 OnChartEvent 的 CHARTEVENT_MOUSE_MOVE 分支,改 def_MouseLineTake / def_MouseLineStop 的前缀名,就能在 MT5 里直接看到拖拽效果。
ChartXYToTimePrice(m_Infos.Id, (class="type">int)lparam, (class="type">int)dparam, w, dt, m_Infos.Price); ObjectMove(m_Infos.Id, def_MouseName, class="num">0, class="num">0, m_Infos.Price = AdjustPrice(m_Infos.Price)); ObjectMove(m_Infos.Id, def_MouseLineName, class="num">0, class="num">0, m_Infos.Price = AdjustPrice(m_Infos.Price)); m_Infos.BtnStatus = (class="type">uint)sparam; if (def_CTRL_Press(m_Infos.BtnStatus) != def_SHIFT_Press(m_Infos.BtnStatus)) { if (!bView) { if (m_Infos.PointsTake > class="num">0) CreateLineH(def_MouseLineTake, m_Infos.CorTake); if (m_Infos.PointsStop > class="num">0) CreateLineH(def_MouseLineStop, m_Infos.CorStop); bView = true; } if (m_Infos.PointsTake > class="num">0) ObjectMove(m_Infos.Id, def_MouseLineTake, class="num">0, class="num">0, m_Infos.Price + (m_Infos.PointsTake * (def_SHIFT_Press(m_Infos.BtnStatus) ? class="num">1 : -class="num">1))); if (m_Infos.PointsStop > class="num">0) ObjectMove(m_Infos.Id, def_MouseLineStop, class="num">0, class="num">0, m_Infos.Price + (m_Infos.PointsStop * (def_SHIFT_Press(m_Infos.BtnStatus) ? -class="num">1 : class="num">1))); } else if (bView) { ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject); bView = class="kw">false; }
重绘与跳出循环的收口写法
在 MT5 自定义指标或脚本的物件绘制循环里,常看到末尾用 ChartRedraw() 紧跟 break 的组合。ChartRedraw() 负责把当前图表缓冲区强制刷新到屏幕,避免物件画完却要等下一个 tick 才显示。 break 则用于在特定条件满足时提前结束 for/while 循环,例如已经定位到目标 K 线或物件数量达标。若漏掉 break,循环会继续扫剩余柱体,徒增 CPU 占用,在品种加载历史较多时可能让脚本卡顿 200ms 以上。 下面这两行是收口处的典型代码片段,直接嵌在你的循环末尾即可: ChartRedraw() 强制重绘图表;break 立即退出循环体,二者顺序不要反,否则可能重绘了多余帧。 外汇与贵金属品种波动剧烈,这类脚本仅作辅助观察,实盘决策仍须自行判断高风险。
ChartRedraw(); class="kw">break; } }
◍ 下一步把触发器接进 EA
眼下这套 EA 还停留在手工操作阶段,核心下单逻辑已经跑通,但缺一个能自动触发动作的开关。作者随文给的 EA_Automatico_-_05.zip 只有 5.67 KB,说明当前版本结构很轻,适合直接在 MT5 里打开对照看。 下一篇计划加触发器,让系统绕过人为决策自动执行部分操作,并最终把手工程序改成全自动 EA。外汇和贵金属杠杆高、滑点随机,自动化前务必先在策略测试器用历史数据验证触发器逻辑,别急着上实盘。 如果你手里已有用别的语言写的简单策略,可以参照这个轻量框架重写成 MQL5,重点先补上触发条件函数,再谈去人工化。