构建自动运行的 EA(第 04 部分):手工触发器(I)·进阶篇
📘

构建自动运行的 EA(第 04 部分):手工触发器(I)·进阶篇

第 2/2 篇

鼠标交互类的事件开关与析构清理

在 MT5 自定义指标或 EA 里接管鼠标行为,第一步是打开图表事件开关。构造函数中对当前图表 ID 调用 ChartSetInteger,把 CHART_EVENT_MOUSE_MOVE 与 CHART_EVENT_OBJECT_DELETE 均置为 true,这样鼠标移动和对象被删的事件才会推送到 DispatchMessage。 析构函数 ~C_Mouse 做反向操作:先把 CHART_EVENT_OBJECT_DELETE 设回 false,再用 ObjectDelete 按 def_MouseName 删掉那条水平线对象,避免残留图形卡在图表上。 GetStatus 用引用传出 m_Infos.Price 与 m_Infos.BtnStatus,外部只需声明 double 与 uint 变量接收,就能知道鼠标悬停价和按键状态,不用反复读全局。 DispatchMessage 里用 switch(id) 分流,当 id 等于 CHARTEVENT_OBJECT_DELETE 且 sparam 等于 def_MouseName 时,说明用户手删了那条线,立刻调 CreateLineH() 重建——这个判断少一个等号就会让线删了不再出现。

MQL5 / C++
ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
CreateLineH();
}
~C_Mouse()
{
ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, false);
ObjectDelete(m_Infos.Id, def_MouseName);
}
class="kw">const class="type">void GetStatus(class="type">class="kw">double &Price, class="type">uint &BtnStatus) class="kw">const
{
Price = m_Infos.Price;
BtnStatus = m_Infos.BtnStatus;
}
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">switch (id)
{
case CHARTEVENT_OBJECT_DELETE:
if (sparam == def_MouseName) CreateLineH();

◍ 鼠标跟价与手数参数的接驳写法

在 EA 的图表事件分支里,CHARTEVENT_MOUSE_MOVE 负责把光标位置转成可下单的价位。下面这段从事件回调中摘出,lparam/dparam 是鼠标像素坐标,经 ChartXYToTimePrice 换算出时间 w 与价格 dt、m_Infos.Price,再用 ObjectMove 把虚拟鼠标对象贴到 AdjustPrice 校准后的报价上。 case CHARTEVENT_MOUSE_MOVE: ChartXYToTimePrice(m_Infos.Id, (int)lparam, (int)dparam, w, dt, m_Infos.Price); ObjectMove(m_Infos.Id, def_MouseName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price)); m_Infos.BtnStatus = (uint)sparam; ChartRedraw(); break; 上面四行是逐行拆解:第一句把像素坐标转为时间价格结构;第二句移动名称为 def_MouseName 的图表对象到第 0 点、校准价;第三句记录鼠标按键状态到 BtnStatus;第四句强制重绘,避免报价线拖影。外汇与贵金属杠杆高,鼠标误触可能瞬间发单,实盘前务必在策略测试器用模拟盘验证。 外部参数部分直接决定仓位与风控:user01=1 为手数步进,user02=100 与 user03=75 是盈亏金额阈值(非点数),user04=true 切日内的日内模式,user05=clrBlack 控制鼠标对象颜色。Magic 号写死 987654321 便于订单归类。 #include <Generic Auto Trader\C_Orders.mqh> #include <Generic Auto Trader\C_Mouse.mqh> C_Orders *manager; C_Mouse *mouse; int OnInit() { manager = new C_Orders(def_MAGIC_NUMBER); mouse = new C_Mouse(user05); 这两段说明订单与鼠标两个类在 OnInit 里以 new 实例化,manager 接 Magic 号、mouse 接颜色输入。开 MT5 把这两头文件放进 Include\Generic Auto Trader\ 路径,编译后拖入图表即可看鼠标报线是否跟随。

MQL5 / C++
case CHARTEVENT_MOUSE_MOVE:
  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));
  m_Infos.BtnStatus = (class="type">uint)sparam;
  ChartRedraw();
  class="kw">break;
class="macro">#include <Generic Auto Trader\C_Orders.mqh>
class="macro">#include <Generic Auto Trader\C_Mouse.mqh>
C_Orders *manager;
C_Mouse *mouse;
class="kw">input class="type">int      user01   = class="num">1;       class=class="str">"cmt">//Lot increase
class="kw">input class="type">class="kw">double   user02   = class="num">100;     class=class="str">"cmt">//Take Profit( FINANCEIRO )
class="kw">input class="type">class="kw">double   user03   = class="num">75;      class=class="str">"cmt">//Stop Loss( FINANCEIRO )
class="kw">input class="type">bool     user04   = true;    class=class="str">"cmt">//Day Trade ?
class="kw">input class="type">color    user05   = clrBlack;class=class="str">"cmt">//Color Mouse
class="macro">#define def_MAGIC_NUMBER class="num">987654321
class="type">int OnInit()
{
  manager = new C_Orders(def_MAGIC_NUMBER);
  mouse = new C_Mouse(user05);

「鼠标与快捷键双通道下单的事件路由」

这段逻辑把图表交互拆成两条触发路径:一条走鼠标对象的状态轮询,一条直接读终端键状态。两者都在 OnChartEvent 里完成,但前者依赖 mouse 指针封装,后者用 TerminalInfoInteger 硬查 CTRL/方向键,互不冲突。 先看鼠标分支。每次事件进来先 DispatchMessage 把系统消息喂给 mouse 对象,再 GetStatus 取回光标价和按键位。只有当 SHIFT 与 CTRL 的按下状态不相等(即只按了其中一个修饰键)时才进挂单判断;左键单击且静态变量 mem 为 0 时,以光标价为基准调 CreateOrder,方向由 SHIFT 决定买或卖,同时把 mem 锁成该价格防止同根 K 线重复触单。 键盘分支则更粗暴:只要 TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL) 为真,上方向键直接 ToMarket 市价买、下方向键市价卖,参数 user01~user04 分别是止损、止盈、手数、魔术码一类外部输入。注意原文第二版 OnChartEvent 里下行键那行少了右括号,编译会报错,需补成 (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) 才能过。 两个 OnChartEvent 重载在真实 EA 里不能同时存在,上面第一段是简化版、第二段是带键盘市价功能的扩展版。你复制时只留一个,否则 MT5 编译器会报函数重定义。外汇与贵金属杠杆高,这类一键下单逻辑回测时滑点可能吃掉 2~5 点,实盘前务必在策略测试器用 tick 级数据跑一遍。

MQL5 / C++
class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   class="kw">delete mouse;
   class="kw">delete manager;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
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="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 (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
   {
      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="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))
   {
      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;
}

把这条线请下神坛

前面拆的事件触发说到底还是半自动:挂单动作得交易者亲手在 MT5 里点下去,EA 本身不会自己往订单簿里塞单。一旦单子进了簿子,后续就脱离人手了——价格触到挂单位,持仓自动生成;再碰限价,持仓自动平掉。 但外汇和贵金属是高风险的,跳空常有。价格瞬间越过挂单位时,持仓可能卡在不利价位,一直扛到经纪商强平或日内休市才了结。所以手工触发不是免死金牌,仓位大小得自己压住。 当前附件 EA_Automatico_-_04.zip(4.83 KB)就是这套逻辑的原始代码,下一篇会改得更好上手。你可以先把它拖进 MT5 的 Experts 目录,开一个 XAUUSD 的 M1 图,手动挂一单看它怎么接力。

常见问题

在OnChartEvent里捕获CHARTEVENT_MOUSE_CLICK,读鼠标坐标转价格,结合输入手数参数调用交易函数即可,不用再开交易面板。
把手数做成input双精度变量,在点击事件里用SymbolInfoDouble校验最小交易量和步进,再传进OrderSend,避免硬写常数。
可以。把EA代码贴给小布,它能标出事件路由里重复触发的分支,并提示加互斥标志位,省你自己逐行排查。
在析构函数~CExpert或OnDeinit里用ObjectDelete按前缀删掉自建图形对象,否则会残留占用图表。
会。需要在事件入口用静态布尔锁或时间戳去重,保证同一行情周期只响应一条通道,别迷信双通道更快。