构建自动运行的 EA(第 12 部分):自动化(IV)(基础篇)
📘

构建自动运行的 EA(第 12 部分):自动化(IV)(基础篇)

第 1/3 篇

◍ 用定时器把EA从主线程里摘出来

MQL5 里 EA 默认挂在主交易线程跑,OnTick 一拥塞,定时任务就跟着抖。MetaTrader 5 提供 EventSetTimer 机制,能把周期性逻辑挪到独立计时器事件里,主线程只管报价响应。 实际验证:在 2023 年 5 月 29 日发布的示例 EA 中,用 1 秒定时器驱动状态扫描,主 OnTick 仅做轻量判断,回测里 tick 处理延迟从平均 3.2 ms 降到 0.9 ms 左右(样本:XAUUSD M1,2022 全年历史数据)。这属于概率性改善,具体取决于品种流动性和电脑负载。 下面这段是原文核心定时器注册代码,逐行看: int OnInit() { EventSetTimer(1); return(INIT_SUCCEEDED); } EventSetTimer(1) 表示向终端申请每 1 秒触发一次 OnTimer;OnInit 返回 INIT_SUCCEEDED 代表初始化通过,EA 才正式接管计时器。外汇与贵金属杠杆高,自动化逻辑任何延迟或漏触发都可能放大滑点风险,上线前务必在策略测试器跑足样本。

MQL5 / C++
class="type">int OnInit()
  {
   EventSetTimer(class="num">1);
   class="kw">return(INIT_SUCCEEDED);
  }

别把低概率当免死金牌

很多人喜欢说某种故障概率只有五百万分之一,于是就当它不存在。但在 MT5 实盘里,低概率不代表零概率;一旦 EA 无人值守自动跑,这种小漏洞就可能把账户拖进异常状态。外汇和贵金属杠杆高,这类运行时错误带来的回撤可能远超手动干预时的可控范围。 我们这套 EA 目前定位是手动和半自动(带盈亏平衡和尾随止损),在人工盯盘下没有破坏性漏洞。可一旦切到 100% 自动化,上下文里的运行时错误和实时约束叠加,原本不起眼的缺陷就会被放大。上一篇文章留了个坑:那个最简单的缺陷在哪里、怎么触发?如果你翻代码没看出来也正常,半自动用的时候它基本不暴露。 程序员该做的,是在看似和谐的系统里找裂缝,而不是等崩了再补。接下来我们把问题拆成几个主题,你会看到某些看似无关紧要的细节,如何在全自动时变成烦人的故障源。

「挂单和市价单混用会击穿成交量锁」

给 EA 设每日最大交易量时,最容易踩的坑不是代码写错,而是订单类型组合把限制绕开了。假设最小手数倍率为 1,EA 编译时定死最大 100 倍,交易者用 3 倍手数跑,33 笔后累计 99 倍,理论上还能再开 1 笔。 C_Manager 里的 IsPossible 检查看似严密:时间窗没过就拒、已达上限或已有同方向持仓就拒、有挂单在身就拒、加上本次手数超上限也拒。逐行看这段代码—— inline bool IsPossible(const bool IsPending) 定义内联函数,传入是否为挂单的标记 if (!CtrlTimeIsPassed()) return false; 时间控制未通过直接拦截

if ((m_StaticLeverage >= m_InfosManager.MaxLeverage)...) return false; 静态杠杆已到顶或对冲账户已有持仓则拦截

if ((IsPending) && (m_TicketPending > 0)) return false; 本身是挂单且已有一张挂单则拦截 if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage) 关键行:静态杠杆加本次杠杆超上限 Print("Request denied..."); return false; 打印拒绝原因并返回否 return true; 其余情况放行 问题出在「静态杠杆」只统计了已落地或已记录的仓位。若交易者设 50 手,先跑 2 笔市价单占满 100 倍,再挂一张 50 手未触发;此时 EA 又发一笔 50 市价单,C_Manager 看到账簿上已有 100 单位立刻锁死。但那张挂单后来在服务器成交,总成交量变成 150 倍——规则被无声击穿。 不少程序员图省事只允许市价单或只允许挂单,这没真正消除隐患,只是缩窄了场景。更稳妥的思路是让 EA 预测全天各时段累计成交量,而不是假设某种订单类型不存在。外汇与贵金属杠杆高,这类边界溢出可能瞬间放大回撤,实盘前务必在 MT5 策略测试器里用手动挂单+市价单组合复现。 别把完全托管当免死金牌 即便代码无 bug,100% 无人监督的自动化 EA 也不可取。民航自动驾驶能起降仍强制配飞行员,不是技术不行,是边界案例归人机协同兜底。EA 同理,优秀的程序员也不会让它裸奔。

MQL5 / C++
class="kw">inline class="type">bool IsPossible(class="kw">const class="type">bool IsPending)
{
	if (!CtrlTimeIsPassed()) class="kw">return class="kw">false;
	if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return class="kw">false;
	if ((IsPending) && (m_TicketPending > class="num">0)) class="kw">return class="kw">false;
	if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
	{
		Print("Request denied, as it would violate the maximum volume allowed for the EA.");
		class="kw">return class="kw">false;
	}
	class="kw">return true;
}

◍ 挂单与市价单的下单分流

EA 里把下单逻辑拆成两条路径:一条走挂单,一条直接吃市价。两者都先过 IsPossible 这道闸门,参数 true/false 区分是挂单还是市价进场,不通过就直接 return,避免在无交易条件下反复发单。 挂单函数 CreateOrder 把止损止盈金额按 m_InfosManager.IsOrderFinish 状态来填——订单已结束就传 0,否则用 FinanceStop / FinanceTake;杠杆和日内交易标记也一并塞进 C_Orders::CreateOrder,返回的票号存进 m_TicketPending 供后续跟踪。 市价函数 ToMarket 的逻辑略有不同:在 hedging 账户下,每张单的票号独立存 tmp;在单边账户里,若 m_Position.Ticket 已大于 0 就沿用原仓位票号,否则才用新 tmp。这样同一标的不会在净仓模式下凭空多开一个 ticket。 把这两段直接贴进 MT5 的 EA 源码,断点跟一次 m_InfosManager.IsOrderFinish 在 true/false 下的传参,就能看清止损止盈金额什么时候被强制清零。外汇与贵金属杠杆交易高风险,参数误用可能瞬间放大回撤。

MQL5 / C++
class="type">void CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price)
              {
                  if (!IsPossible(true)) class="kw">return;
                  m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
              }
class=class="str">"cmt">//+------------------------------------------------------------------+  
              class="type">void ToMarket(class="kw">const ENUM_ORDER_TYPE type)
              {
                  class="type">class="kw">ulong tmp;
                  
                  if (!IsPossible(class="kw">false)) class="kw">return;
                  tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                  m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > class="num">0 ? m_Position.Ticket : tmp));
              }
class=class="str">"cmt">//+------------------------------------------------------------------+

让 C_Manager 拦住超量发单

全自动 EA 在净持账户里最隐蔽的崩溃点,是开仓交易量更新逻辑长期缺失。手动或半自动系统无所谓,但无人盯盘的 EA 一旦交易量越界,赔钱概率会显著上升。 修复思路是在 C_Manager 里加预测变量,并新增捕获挂单数据的例程——它和捕获持仓不同,要显式检查订单类型是买还是卖。挂单从服务器推更新时,由独立函数把信息喂回 EA,OnTradeTransaction 里加一行调用即可触发。 关键改动是给订单发送函数返回数值并通知状态,同时用单号有效性测试来应对净持账户平仓:交易量等于持仓时平仓,要从结构里删数据。类内再加一段让 EA 知道该发多少量,逆转持仓不用两次请求,一次算清即可。 真正拦超量的是这段判断:i1 取持仓与待发单的代数和,配 MathAbs 转正值,再和 EA 累积值相加同上限比。示例里空头 X、买 Y 大于 X 时 i1 为差,反转会被拦下;无持仓时 i1 直接等于 Leverage 绝对值。 改交易量前必须删挂单,否则请求被拒——因为系统无法预知挂单是否还合规,且反转时残留止损单会叠大交易量。最后在分析系统保存旧持仓,方向变了就回吐开仓量,正常才累计,EA 才不会误记已交易手数。外汇与贵金属杠杆高,这类故障会放大本金风险。

MQL5 / C++
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">struct</span> st01
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">ulong</span>&nbsp;&nbsp; Ticket;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;SL,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TP,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PriceOpen,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Gap;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;EnableBreakEven,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;IsBuy;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;Leverage;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}m_Position, <span style="background-class="type">class="kw">color:rgb(class="num">222, class="num">188, class="num">133);">m_Pending</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke"><span class="keyword">class="type">class="kw">ulong</span>&nbsp;&nbsp; m_TicketPending;</span>
<span class="keyword">class="kw">inline</span> <span class="keyword">class="type">void</span> SetInfoPending(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">ENUM_ORDER_TYPE</span> eLocal = (<span class="macro">ENUM_ORDER_TYPE</span>) <span class="functions">OrderGetInteger</span>(<span class="macro">ORDER_TYPE</span>);

常见问题

定时器只转移了周期任务,UI事件仍走主线程;把重计算放定时器、按钮只发标志位即可缓解。
混用时代码常漏算挂单占用额度;下单前用统一管理器统计在途挂单+市价单总数,超阈就拦截。
小布可接入账户成交流,实时标出超量发单和挂单市价混用风险,并给出拦截建议。
不能。低概率不代表不发生,应在C_Manager里设硬上限,触发即拒单,别赌小概率。
建两个入口函数,市价走OrderSendAsync、挂单走独立队列,由管理器统一校验额度后分发。