构建自动运行的 EA(第 05 部分):手工触发器(II)(基础篇)
📘

构建自动运行的 EA(第 05 部分):手工触发器(II)(基础篇)

第 1/3 篇

◍ 用对象事件给 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 数据跑一遍,观察点击延迟下的成交偏差。

MQL5 / C++
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 实盘前,先在策略测试器里验证这套继承链编译无误,高风险品种别省检查。

MQL5 / C++
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 的返回值就能验证这套封装是否贴着你的经纪商参数。外汇和贵金属杠杆波动大,参数读错会直接放大仓位风险。

MQL5 / C++
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 里按位拆键位,写交互脚本时不用自己再查文档。

MQL5 / C++
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,调用统一走父类实现,避免两套重复逻辑在两边不同步。改完之后代码稳定性同级,但健壮性强了一截——以前改一处漏一处的情况少了很多。下一步要补的是手工下单时的止盈止损限价线,这部分留到新章节单独拆。

MQL5 / C++
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)

常见问题

用对象点击事件(OBJEVENT_CLICK)给EA发手工触发器,点一下按钮就切换运行状态,不必改代码重编译。
用水平线对象存参考价,EA每 tick 读对象坐标转成价格,触及即按预设逻辑处理,省去硬编码。
小布可识别图表上的手动触发对象与参考线,提示遗漏或冲突,并把重复诊断交给它看,你只管决策。
用合约规格算每点价值,再除账户货币当量,得到金融量对应跳动点数,写个换算函数放初始化里即可。
按品种Digits取最小变动单位,乘合约大小与报价比得出每点价值,用常量锁死父类数据避免被误改。