构建自动运行的 EA(第 05 部分):手工触发器(II)·综合运用
🖱️

构建自动运行的 EA(第 05 部分):手工触发器(II)·综合运用

(3/3)·从重复代码里抽出止盈止损线逻辑,让手工 EA 和自动 EA 共用同一套底层而不互相拖垮

案例拆解新手友好 第 3/3 篇
很多初学者在手工 EA 里直接堆鼠标事件和订单代码,下次写自动 EA 又从头复制一遍,结果改一处崩两处。把图表指示线和下单逻辑混在一起,是新手最常踩的坑。本篇收尾整个系列,先把这套边界切干净。

「把止盈止损线塞进鼠标类里」

在 MT5 里做交互式挂单辅助线,最省事的位置不是 EA 的 OnChartEvent,而是 C_Mouse 类自身的事件处理程序。鼠标移开图表,示意线跟着消失,不用额外写清理逻辑,这是把 TP/SL 横线托管给鼠标类最直接的收益。 原本的宏定义要重做。旧的单一名字宏删掉,换成带前缀的 MOUSE_T 和 MOUSE_S,分别代表止盈、止损横线;再补两个颜色变量存线色,创建时只需传名字和颜色,代码量一下砍掉大半。 构造函数现在要比以前多收几个参数——主要是把财务意义上的止损止盈值直接转成点数存进变量,而不是等 EA 每次调用函数去算。直接读变量比调函数快,鼠标移动事件高频触发时这点延迟差异肉眼可感。 析构侧只改负责删对象的函数:按前缀批量删,凡是名字以 MOUSE_ 开头的 HLINE 一并清除,以后扩展别的鼠标线也能复用。 关键交互在按键判断:SHIFT 或 CTRL 单独按住时才画 TP/SL 线,俩键同时按忽略。若线已存在先删掉,MT5 会立刻发对象删除事件,处理完再把价格线贴回图表;第一次创建要求数值大于 0,避免图表上漂着怪线,创建后打标记,后续移动只更新位置不重建。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
    class="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。外汇与贵金属波动剧烈,杠杆会放大点位换算误差,实盘前先用策略测试仪验证映射。

MQL5 / C++
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 上手动删掉参考线,大概率会看到它在原价格或记录价瞬间重生;外汇与贵金属杠杆高,这类辅助线只管参考、不参与下单风控。

MQL5 / C++
  ~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 里直接看到拖拽效果。

MQL5 / C++
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 立即退出循环体,二者顺序不要反,否则可能重绘了多余帧。 外汇与贵金属品种波动剧烈,这类脚本仅作辅助观察,实盘决策仍须自行判断高风险。

MQL5 / C++
                                                                              ChartRedraw();
                                                                              class="kw">break;
                                                                        }
                                                                }

◍ 下一步把触发器接进 EA

眼下这套 EA 还停留在手工操作阶段,核心下单逻辑已经跑通,但缺一个能自动触发动作的开关。作者随文给的 EA_Automatico_-_05.zip 只有 5.67 KB,说明当前版本结构很轻,适合直接在 MT5 里打开对照看。 下一篇计划加触发器,让系统绕过人为决策自动执行部分操作,并最终把手工程序改成全自动 EA。外汇和贵金属杠杆高、滑点随机,自动化前务必先在策略测试器用历史数据验证触发器逻辑,别急着上实盘。 如果你手里已有用别的语言写的简单策略,可以参照这个轻量框架重写成 MQL5,重点先补上触发条件函数,再谈去人工化。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 事件冲突和冗余类提示,你专注决策而不是翻代码。

常见问题

平台只在订单到达服务器后才接管显示;鼠标下单瞬间限价位置由 EA 用水平线预画,否则交易者看不到挂单落点。
本篇把后两者里重复的画线和下单辅助抽进 C_Terminal,原来类只保留各自特有逻辑,降低未来删除手工模块的成本。
隔离后概率上不会,因为依赖已收拢到 C_Terminal;只要自动 EA 不调用 OnChartEvent 相关接口,移除较安全。
可以,小布盯盘的品种页会标记事件处理器与订单类的耦合度,方便你判断要不要按本篇做类拆分。
外汇贵金属杠杆高、滑点随机,手工触发器仅降低操作失误概率,实盘前请用模拟盘验证至少数日。