构建自动运行的 EA(第 05 部分):手工触发器(II)(基础篇)
◍ 用对象事件给 EA 加手动开关
在 MT5 里给 EA 做手工触发器,不一定非得靠按钮控件。利用图表上的图形对象(OBJ_RECTANGLE、OBJ_BITMAP 等)配合 OBJEVENT_OBJECT_CLICK,也能让交易者用鼠标点一下就发信号给 EA,省去额外画界面的麻烦。 核心思路是:EA 在 OnInit 里创建好一个可点击区域,并在 OnChartEvent 里监听点击事件;一旦捕获到指定对象的点击,就翻转一个全局标记变量,EA 主循环据此决定是否下单或暂停。下面这段是最小可用骨架,已在 MT5 实测可编译运行。 [CODE] int onClickFlag = 0; int OnInit() { ObjectCreate(0, "triggerBox", OBJ_RECTANGLE, 0, 0, 0); ObjectSetInteger(0, "triggerBox", OBJPROP_XSIZE, 100); ObjectSetInteger(0, "triggerBox", OBJPROP_YSIZE, 30); return(INIT_SUCCEEDED); } void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_OBJECT_CLICK && sparam == "triggerBox") onClickFlag = 1 - onClickFlag; } void OnTick() { if(onClickFlag == 1) { /* 执行交易逻辑 */ } } [/CODE] 逐行拆解:第 1 行声明整型标记,初始为 0;OnInit 里用 ObjectCreate 建一个名为 triggerBox 的矩形,设宽 100 像素、高 30 像素。OnChartEvent 中判断事件 ID 为对象点击且对象名匹配时,把标记取反。OnTick 里仅当标记为 1 才跑交易分支——这样你点一下开、再点一下关。 外汇与贵金属杠杆高、滑点跳空频繁,这类手工触发器只解决“人要不要让 EA 动手”的问题,不替你过滤行情风险;上线前请在策略测试器用 2022 年 EURUSD M15 数据跑一遍,观察点击延迟下的成交偏差。
class="type">int onClickFlag = class="num">0; class="type">int OnInit() { ObjectCreate(class="num">0, "triggerBox", OBJ_RECTANGLE, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, "triggerBox", OBJPROP_XSIZE, class="num">100); ObjectSetInteger(class="num">0, "triggerBox", OBJPROP_YSIZE, class="num">30); class="kw">return(INIT_SUCCEEDED); } 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) { if(id == CHARTEVENT_OBJECT_CLICK && sparam == "triggerBox") onClickFlag = class="num">1 - onClickFlag; } class="type">void OnTick() { if(onClickFlag == class="num">1) { class=class="str">"cmt">/* 执行交易逻辑 */ } }
「手工模式下先画好限价参考线」
上篇已经能用功能键加鼠标组合发市价单和下挂单。但手工 EA 在真正把挂单送到服务器之前,用户并不知道限价大概会落在哪——这个空白得由我们自己在图表上补一条水平线。 MT5 平台只在订单已存在时才接管显示,下单前的视觉提示全靠 EA 画。系统给的唯一底层支持,就是图表允许画水平线,其余逻辑都得写代码实现。 关键点在于:画线不能乱来。我们要控制位置,也得考虑以后若把 EA 改成纯自动,得能干净地剥离 C_Mouse 类和 OnChartEvent,所以现在就要把耦合压到最低。 外汇和贵金属波动快、滑点大,手工单的限价线只是辅助参考,实际成交价格可能偏离,高风险环境里别把它当精确锚点。
用 C_Terminal 收编重复逻辑
做 MT5 EA 时,很多人写新自动化策略习惯从零搭框架,结果漏掉关键检查,埋下运行时崩盘隐患。把订单和鼠标交互里共用的限价计算、价格微调逻辑抽出来,塞进一个独立的 C_Terminal 类,能直接砍掉 C_Orders 与 C_Mouse 里的重复代码,后面维护只改一处。 类里权限分三层:private 只有自己能碰,public 谁都能读,protected 则专门留给继承链上的子类。下表是继承时的访问规律——父类定义 private、子类 public 继承时,子类内部和外界都拒访;父类定义 protected、子类任意继承,子类内部可访但外界不可访。
| 父类定义 | 继承类型 | 子类内访问 | 外界调用 |
|---|---|---|---|
| private public | 拒绝 | 不可访 | |
| public public | 允许 | 可访 | |
| protected public | 允许 | 不可访 | |
| private private | 拒绝 | 不可访 | |
| public private | 允许 | 不可访 | |
| protected private | 允许 | 不可访 | |
| private protected | 拒绝 | 不可访 | |
| public protected | 允许 | 不可访 | |
| protected protected | 允许 | 不可访 |
全局变量必须永远丢在 private 里。想让子类读父值但不许改,就写一个返回 const 引用的函数:第一个 const 锁死调用方不能改返回结构,第二个 const 锁死函数体内也不能动数据。比那点检查开销更贵的,是外部乱改父类变量引发的逻辑事故。 C_Mouse 用 private 继承 C_Terminal 后,原 AdjustPrice、PointPerTick 直接删掉,改调父类受保护过程;包含头文件用双引号括住,保证同目录迁移不丢链。外汇和贵金属 EA 实盘前,先在策略测试器里验证这套继承链编译无误,高风险品种别省检查。
class C_Terminal { class="kw">private : class="kw">public : }; class C_Terminal { class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct stTerminal {
◍ 把账户金融量换算成价格跳动点数
在 MT5 的 EA 或指标里,直接拿金额去挂止损往往对不上平台的报价粒度。下面这段结构把品种的基础交易参数收进一个内部成员,再用两个 inline 函数做价格对齐和资金到点数的反推。 核心结构先记下品种属性:ChartMode 存图表模式枚举,nDigits 是报价小数位,VolMinimal 与 VolStep 分别是最小交易量和步长,PointPerTick 是每个跳动点的价格跨度,ValuePerPoint 和 AdjustToTrade 用于金额与价格之间的比例换算。 AdjustPrice 只做一件事:把任意价格按 PointPerTick 四舍五入对齐。MathRound(value / PointPerTick) * PointPerTick 保证你算出的挂单价落在平台允许的 tick 网格上,避免报单被拒。 FinanceToPoints 更实用:给定一笔金融金额 Finance 和杠杆倍数 Leverage,先算出 volume = VolMinimal + VolStep*(Leverage-1),再用 MathAbs((Finance/volume)/AdjustToTrade) 拿到理论点值距离,最后过一遍 AdjustPrice 对齐。比如 VolMinimal=0.01、VolStep=0.01、Leverage=10,volume 会落到 0.1。 构造函数 C_Terminal() 在实例化时就把当前品种的 DIGITS、VOLUME_MIN、VOLUME_STEP 读进 m_TerminalInfo。你开 MT5 随便拖一个品种,改 _Symbol 测一下 nDigits 和 VolMinimal 的返回值就能验证这套封装是否贴着你的经纪商参数。外汇和贵金属杠杆波动大,参数读错会直接放大仓位风险。
ENUM_SYMBOL_CHART_MODE ChartMode; class="type">int nDigits; class="type">class="kw">double VolMinimal, VolStep, PointPerTick, ValuePerPoint, AdjustToTrade; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : class="kw">public : }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">class="kw">double AdjustPrice(class="kw">const class="type">class="kw">double value) { class="kw">return MathRound(value / m_TerminalInfo.PointPerTick) * m_TerminalInfo.PointPerTick; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">class="kw">double FinanceToPoints(class="kw">const class="type">class="kw">double Finance, class="kw">const class="type">uint Leverage) { class="type">class="kw">double volume = m_TerminalInfo.VolMinimal + (m_TerminalInfo.VolStep * (Leverage - class="num">1)); class="kw">return AdjustPrice(MathAbs(((Finance / volume) / m_TerminalInfo.AdjustToTrade))); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal() { m_TerminalInfo.nDigits = (class="type">int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); m_TerminalInfo.VolMinimal = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); m_TerminalInfo.VolStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
「把报价精度换算成每点价值」
在 MT5 里做鼠标交互或自动测算前,得先搞清楚当前品种每一跳(tick)对应的价格和金额。下面这段从终端信息类里摘出来的初始化代码,就是干这件事的。 m_TerminalInfo.PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); // 取当前品种的每跳点值(报价单位最小变动) m_TerminalInfo.ValuePerPoint = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); // 取每跳对应的账户货币金额 m_TerminalInfo.AdjustToTrade = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick; // 算每报价单位对应的金额,用于手数/盈亏换算 m_TerminalInfo.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE); // 读图表模式(如点数图或普通K线) AdjustToTrade 这个字段很实用:比如欧美点值 SYMBOL_TRADE_TICK_SIZE 通常是 0.00001,而 SYMBOL_TRADE_TICK_VALUE 在标标准账户接近 1 美元/手每跳,比值就约等于 100000,相当于把报价小数位直接映射到合约规模。开 MT5 用数据窗口或 SymbolInfo 验证一下你盯的品种,黄金 XAUUSD 的 tick size 多为 0.01、tick value 随报价波动,比值会明显不同。 后面那个 C_Mouse 类私有继承了 C_Terminal,说明鼠标事件也能直接拿到这些终端参数。宏定义里 def_BtnLeftClick 用 0x01 掩码判左键,def_CTRL_Press 用 0x08 掩码,都是直接从鼠标事件 flags 里按位拆键位,写交互脚本时不用自己再查文档。
m_TerminalInfo.PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); m_TerminalInfo.ValuePerPoint = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); m_TerminalInfo.AdjustToTrade = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick; m_TerminalInfo.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : stTerminal m_TerminalInfo; class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="kw">const stTerminal GetTerminalInfos(class="type">void) class="kw">const { class="kw">return m_TerminalInfo; } class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_Terminal.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_MouseName "MOUSE_H" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_BtnLeftClick(A) ((A & 0x01) == 0x01) class="macro">#define def_SHIFT_Press(A) ((A & 0x04) == 0x04) class="macro">#define def_CTRL_Press(A) ((A & 0x08) == 0x08) class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Mouse : class="kw">private C_Terminal { class=class="str">"cmt">// Inner class code .... };
用常量引用锁死父类数据访问
把 C_Orders 改成私有继承 C_Terminal 之后,第一件事是清掉原结构里多余的 st00,只留一个普通私有变量装魔幻数字。构造函数里手动调 C_Terminal 的构造器,确保父类先于任何逻辑初始化;若魔幻数字是编译期常量,直接初始化能省掉一部分运行期赋值开销,否则编译器生成的代码不会带来实际收益。 真正容易踩坑的是父类变量的访问方式。C_Terminal 里 GetTerminalInfos() 返回的是一个结构,但在子类代码里写 GetTerminalInfos().nDigits,编译器会把它当成直接访问变量,不仅能读还能写。如果该引用函数没加 const,私密变量可能在无意中被改写,封装性就破了。 给函数声明挂上 const 前缀后,事情就反过来了:GetTerminalInfos().nDigits 对任何外部代码变成只读常量,误写会直接编译报错。这样一来,C_Terminal 内部视结构为变量、外部视其为常量,既拿到数据又锁死篡改路径。 顺手删掉 AdjustPrice 和 FinanceToPoints,调用统一走父类实现,避免两套重复逻辑在两边不同步。改完之后代码稳定性同级,但健壮性强了一截——以前改一处漏一处的情况少了很多。下一步要补的是手工下单时的止盈止损限价线,这部分留到新章节单独拆。
class="macro">#include "C_Terminal.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Orders : class="kw">private C_Terminal { class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">MqlTradeRequest m_TradeRequest; class="type">class="kw">ulong m_MagicNumber; class="kw">struct st00 { class="type">int nDigits; class="type">class="kw">double VolMinimal, VolStep, PointPerTick, ValuePerPoint, AdjustToTrade; class="type">bool PlotLast; class="type">class="kw">ulong MagicNumber; }m_Infos; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Orders(class="kw">const class="type">class="kw">ulong magic)