构建自动运行的 EA(第 12 部分):自动化(IV)·进阶篇
🛡️

构建自动运行的 EA(第 12 部分):自动化(IV)·进阶篇

(2/3)· 每日交易量上限背后那处看似无害、却能让 100% 自动化 EA 直接失控的缺陷

案例拆解 第 2/3 篇
多数人以为把半自动 EA 直接切到全自动只是改个开关,实则一处每日交易量限制的逻辑缺口会在无人监督时放大成真实故障。低概率不代表零后果,忽略它等于把账户交给看不见的定时器。外汇与贵金属杠杆交易本就高风险,自动化更不能替你免掉盯盘责任。

「挂单状态同步的底层钩子」

在 EA 里跟踪挂单,不能靠定时器轮询,得直接挂到交易事务回调上。下面这段把 ORDER_UPDATE 事件接到了 manager.UpdatePending,订单改价、改手数时结构体内的缓存才会跟着刷。 杠杆字段用当前体积除以终端最小体积算出整数倍:(uint)(OrderGetDouble(ORDER_VOLUME_CURRENT)/GetTerminalInfos().VolMinimal),这一步在微型账户上容易因 VolMinimal 为 0.01 而拿到整十倍数值,复制时先打印验证。 方向判断把四种买类订单全归到 IsBuy,卖类同理留空。外汇与贵金属杠杆放大波动,挂单被扫或拒的概率随点差跳变而上升,回测和实盘要用不同滑点假设。 OnTradeTransaction 里 ORDER_DELETE 分支用 trans.order==trans.position 区分挂单转成仓还是纯删挂单,逻辑若写反会在部分券商成交回报延迟时漏更持仓。

MQL5 / C++
m_Pending.Leverage = (class="type">uint)(OrderGetDouble(ORDER_VOLUME_CURRENT) / GetTerminalInfos().VolMinimal);
m_Pending.IsBuy = ((eLocal == ORDER_TYPE_BUY) || (eLocal == ORDER_TYPE_BUY_LIMIT) || (eLocal == ORDER_TYPE_BUY_STOP) || (eLocal == ORDER_TYPE_BUY_STOP_LIMIT));
m_Pending.PriceOpen = OrderGetDouble(ORDER_PRICE_OPEN);
m_Pending.SL = OrderGetDouble(ORDER_SL);
m_Pending.TP = OrderGetDouble(ORDER_TP);
}
class="type">void UpdatePending(class="kw">const class="type">class="kw">ulong ticket)
{
   if ((ticket == class="num">0) || (ticket != m_Pending.Ticket) || (m_Pending.Ticket == class="num">0)) class="kw">return;
   if (OrderSelect(m_Pending.Ticket)) SetInfoPending();
}
class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result)
{
   class="kw">switch (trans.type)
   {
      case TRADE_TRANSACTION_POSITION:
         manager.UpdatePosition(trans.position);
         class="kw">break;
      case TRADE_TRANSACTION_ORDER_DELETE:
         if (trans.order == trans.position) (*manager).PendingToPosition();
         else (*manager).UpdatePosition(trans.position);
         class="kw">break;
      case TRADE_TRANSACTION_ORDER_UPDATE:
         (*manager).UpdatePending(trans.order);
         class="kw">break;

成交回执里怎么同步持仓状态

EA 在 OnTradeTransaction 里拿到 TRADE_TRANSACTION_REQUEST 事件后,不能无脑刷新,要先过滤是不是自己下的单。下面这段逻辑只处理当前品种、魔数匹配且回执码为 TRADE_RETCODE_DONE 的请求,再按 action 类型分流。 市价成交(TRADE_ACTION_DEAL)和改止盈止损(TRADE_ACTION_SLTP)都走 UpdatePosition,前者用 request.order、后者用 trans.position 作定位;撤掉挂单(TRADE_ACTION_REMOVE)则调 EraseTicketPending 清掉本地 ticket 缓存。这样本地持仓映射和终端实际状态偏差概率更低。 启动阶段也别信历史缓存。LoadPositionValid 用 PositionGetTicket 从尾到头倒序扫 PositionsTotal(),碰到 ticket 为 0 或非本品种就 continue,只把真实存在的本符号持仓载入 manager。外汇和贵金属杠杆高,实盘跑前建议在策略测试器用不同魔数多 EA 同图验证一次事件过滤是否串号。

MQL5 / C++
case TRADE_TRANSACTION_REQUEST: if ((request.symbol == _Symbol) && (result.retcode == TRADE_RETCODE_DONE) && (request.magic == def_MAGIC_NUMBER)) class="kw">switch (request.action)
  {
    case TRADE_ACTION_DEAL:
      (*manager).UpdatePosition(request.order);
      class="kw">break;
    case TRADE_ACTION_SLTP:
      (*manager).UpdatePosition(trans.position);
      class="kw">break;
    case TRADE_ACTION_REMOVE:
      (*manager).EraseTicketPending(request.order);
      class="kw">break;
  }
  class="kw">break;
}
class="kw">inline class="type">void LoadPositionValid(class="type">void)
  {
    class="type">class="kw">ulong value;
    
    for (class="type">int c0 = PositionsTotal() - class="num">1; (c0 >= class="num">0) && (_LastError == ERR_SUCCESS); c0--)
    {
      if ((value = PositionGetTicket(c0)) == class="num">0) class="kw">continue;
      if (PositionGetString(POSITION_SYMBOL) != _Symbol) class="kw">continue;

◍ 持仓遍历与挂单创建的魔术码过滤

EA 在扫描账户持仓时,第一道关卡就是魔术码比对:只有 PositionGetInteger(POSITION_MAGIC) 等于本 EA 的 GetMagicNumber() 才进入后续处理,否则直接 continue 跳过。这一步能避免把手动单或其它 EA 的仓位误当成自己的仓位去平仓或统计。 当账户处于对冲模式(m_bAccountHedging 为真)且已有一张挂单 ticket(m_TicketPending > 0)时,代码会调用 C_Orders::ClosePosition(value) 把当前遍历到的持仓平掉并 continue,这常用于先清场再下新单的逻辑。若不是对冲场景,则检查 m_Position.Ticket:大于 0 说明已有持仓却还想建仓,直接 SetUserError(ERR_Unknown) 报错;否则把 value 记进 m_Position.Ticket,刷新持仓信息并把当前杠杆存到 m_StaticLeverage。 真正下单走 CreateOrder():先 IsPossible(true) 做前置校验,不通过直接返回 false;通过后才用 C_Orders::CreateOrder 发单,其中止损止盈金额取决于 m_InfosManager.IsOrderFinish——未完成订单才带 FinanceStop / FinanceTake,杠杆与日内交易标志也从 m_InfosManager 取。外汇与贵金属杠杆可调,但都是高风险品种,参数乱设可能瞬间放大回撤。 把下面这段贴进 MT5 策略测试器,改 GetMagicNumber() 返回值就能看到不同魔术码下单是否被隔离:

MQL5 / C++
if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) class="kw">continue;
if ((m_bAccountHedging) && (m_TicketPending > class="num">0))
  {
  C_Orders::ClosePosition(value);
  class="kw">continue;
  }
if (m_Position.Ticket > class="num">0) SetUserError(ERR_Unknown); else
  {
  m_Position.Ticket = value;
  SetInfoPositions();
  m_StaticLeverage = m_Position.Leverage;
  }
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price)
  {
  class="type">bool bRet = class="kw">false;
  if (!IsPossible(true)) class="kw">return bRet;
  m_Pending.Ticket = 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);

「挂单与市价单的票号回填逻辑」

这段实现里,挂单和市价单在开仓后都做了一次票号有效性校验。若 m_Pending.Ticket 大于 0,才调用 OrderSelect 选中该挂单,选中成功再刷新挂单信息结构体,否则直接返回 false,避免后续对空票号误操作。 市价单走 ToMarket 时,先过 IsPossible(false) 这道闸门,不通过就原样返回。真正下单后把返回的 tmp 票号写进 m_Position.Ticket;在锁仓账户(m_bAccountHedging 为 true)中直接采用 tmp,非锁仓账户则优先保留已有持仓票号,仅在原票号为 0 时才接 tmp。 选中持仓用 PositionSelectByTicket,失败就 ZeroMemory 清空 m_Position 结构体。外汇与贵金属杠杆交易高风险,票号回填失败可能意味着下单未确认,应在 MT5 策略测试器里打印 m_Position.Ticket 验证分支是否按账户类型走对。 GetVolumeInPosition 直接返回 m_Position.Leverage,这个命名容易误读——它吐出的是持仓杠杆字段而非手数,复制代码时注意别把 Leverage 当成交量用。

MQL5 / C++
if (m_Pending.Ticket > class="num">0) bRet = OrderSelect(m_Pending.Ticket);
if (bRet) SetInfoPending();

class="kw">return bRet;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type)
  {
   class="type">class="kw">ulong tmp;
   class="type">bool bRet = class="kw">false;

   if (!IsPossible(class="kw">false)) class="kw">return bRet;
   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));
   if (m_Position.Ticket > class="num">0) bRet = PositionSelectByTicket(m_Position.Ticket);
   if (!bRet) ZeroMemory(m_Position);

   class="kw">return bRet;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">const class="type">uint GetVolumeInPosition(class="type">void) class="kw">const
  {
   class="kw">return m_Position.Leverage;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

挂单与市价单的下单封装差异

在 MT5 的 EA 架构里,把下单逻辑收口到两个成员函数能省掉大量重复判断:CreateOrder 负责挂单,ToMarket 负责即时成交,两者都先走 IsPossible 做杠杆与品种可行性校验。 CreateOrder 多接收一个 Price 参数,用于限价或停止单的触发价;若调用方没传 LeverageArg(默认 0),就回落到 m_InfosManager.Leverage 取账户实际杠杆。订单创建后用 OrderSelect 回选票号,成功才写 SetInfoPending 更新挂单状态。 ToMarket 不收价格参数,直接吃 C_Orders::ToMarket 的返回票号;在 hedging 账户下 m_Position.Ticket 直接等于 tmp,而净仓账户若已有持仓票号则保留原值,避免重复开仓。外汇与贵金属杠杆可调到 1:500 以上,穿仓风险极高,回测时建议先用 IsPossible 拦掉超杠杆请求。 下面两段是原函数的骨架,逐行看更清楚封装思路。

MQL5 / C++
class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">uint LeverageArg = class="num">0)
{
   class="type">bool bRet = class="kw">false;
   if (!IsPossible(type, (LeverageArg > class="num">0 ? LeverageArg : m_InfosManager.Leverage))) class="kw">return bRet;
   m_Pending.Ticket = 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);
   if (m_Pending.Ticket > class="num">0) bRet = OrderSelect(m_Pending.Ticket);
   if (bRet) SetInfoPending();
   class="kw">return bRet;
}

class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">uint LeverageArg = class="num">0)
{
   class="type">class="kw">ulong tmp;
   class="type">bool bRet = class="kw">false;
   if (!IsPossible(type, (LeverageArg > class="num">0 ? LeverageArg : m_InfosManager.Leverage))) class="kw">return bRet;
   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));
把边界检查交给小布
这些边界与运行时弱点的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时预警,你只需判断要不要人工介入。

常见问题

半自动时有人工在环路里兜底,交易量上限等逻辑缺口被手动操作掩盖;全自动去掉了人,缺口在极端序列里直接触发失控,概率虽低但后果真实。
当挂单部分成交、回撤重发、或跨品种统计遗漏时,计数器可能漏算或重复算,导致实际敞口超过预设,属于典型边界处理不足。
可以,小布盯盘会在品种页标记异常成交密度与逻辑边界越界倾向,把重复劳动交给小布,你专注决策即可。
runtime error 通常打断执行反而显眼;逻辑缺陷静默累积,往往在自动化跑了一段时间后才暴露,隐蔽性更高。
上篇减少的是崩溃类故障,本篇指向业务规则自身的盲区,两者叠加才接近可监督的自动化,而非免监督。