构建自动运行的 EA(第 11 部分):自动化(III)·综合运用
📘

构建自动运行的 EA(第 11 部分):自动化(III)·综合运用

第 3/3 篇

挂单转持仓时的对冲平仓分支

在开启账户对冲模式(m_bAccountHedging 为真)且已有持仓票号大于 0 时,PendingToPosition 会先调用 ResetLastError 清掉上一笔错误码,再判断挂单流程是否已结束(IsOrderFinish)。 若流程结束,代码会尝试用 PositionSelectByTicket 选中旧持仓并 ClosePosition(m_Position.Ticket) 平掉它,随后无参 ClosePosition() 再兜底清一次;同时若新挂单票号 m_TicketPending 还能被选中就走 C_Orders::ClosePosition 平仓,否则 RemoveOrderPendent 撤单。 注意 ZeroMemory(m_Position.Ticket) 会把持仓结构票号清零,m_TicketPending 置 0,最后再 ResetLastError——这一步若遗漏,后续下单可能带着脏票号报错。外汇与贵金属杠杆交易高风险,这类平仓逻辑在极端滑点下可能平不掉,需在 MT5 策略测试器用真实点差回测验证。

MQL5 / C++
class="type">void PendingToPosition(class="type">void)
  {
  ResetLastError();
  if ((m_bAccountHedging) && (m_Position.Ticket > class="num">0))
    {
    if (m_InfosManager.IsOrderFinish)
      {
      if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket);
      ClosePosition();
      if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
      ZeroMemory(m_Position.Ticket);
      m_TicketPending = class="num">0;
      ResetLastError();
      }else  SetUserError(ERR_Unknown);
    }else m_Position.Ticket = (m_Position.Ticket == class="num">0 ? m_TicketPending : m_Position.Ticket);
  m_TicketPending = class="num">0;

◍ 挂单 ticket 的清理与容错

这段代码处理的是 pending 订单生命周期里最容易漏掉的一环:当某张挂单的 ticket 失效时,如何安全清场。 函数 EraseTicketPending 先判断传入 ticket 是否等于当前记录的 m_TicketPending 且大于 0,避免误清其他单。若条件成立,就尝试用 PositionSelectByTicket 按 ticket 选中对应仓位;选到了就直接 ClosePosition 平掉,选不到则走 RemoveOrderPendent 撤掉挂单,最后把 m_TicketPending 归零。 外层还调了 ResetLastError 清空错误码,并用一行三元表达式兜底——若传入 ticket 正好等于记录值则置 0,否则保留原记录。这种双保险能降低 EA 在订单状态跳变时卡死的概率,外汇与贵金属市场跳空频繁,此类容错直接关系实盘稳定性,属高风险场景下的必要设计。 开 MT5 把这段塞进你的订单管理类,跑一遍模拟盘看 m_TicketPending 在撤单后是否准时归零,比读十遍文档管用。

MQL5 / C++
if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
      CheckToleranceLevel();
    }
class="kw">inline class="type">void EraseTicketPending(const class="type">ulong ticket)
  {
    if ((m_TicketPending == ticket) && (m_TicketPending > class="num">0))
      {
        if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending);
        else RemoveOrderPendent(m_TicketPending);
        m_TicketPending = class="num">0;
      }
    ResetLastError();
    m_TicketPending = (ticket == m_TicketPending ? class="num">0 : m_TicketPending);
  }

「杠杆叠加下的隐性超量开仓」

EA 里有个容易被忽略的漏洞:最大确定交易量设为最小交易量的 10 倍,但交易者若在第三次操作配了 3 倍杠杆,EA 已逼近上限;发第四个请求时便突破允许最大值。更极端地,若配了 9 倍杠杆,EA 实际执行的第 2 笔就可能超过最大交易量 8 倍——这种自动化下的失控,手动交易员靠肉眼能拦住,EA 不会。 外汇与贵金属杠杆交易本身高风险,自动化 EA 若不清算累计杠杆,爆量开仓的概率会随请求次数非线性上升。修复思路是把发单逻辑收进一个私有函数,用参数区分市价单与挂单,发送失败返 false、成功返 true,从而删掉原先散落各处的重复代码。 下面这段 UpdatePosition 是重构后私有区的真实代码,负责按持仓票更新挂单价格: 别把重复代码当无害 三处几乎一样的发单片段,用宏或函数收口都行;选函数是因为读者多刚入门,函数比宏更易单步跟踪。聚合后所有划掉的行直接删,程序更稳,也少一处未来改漏的地方。

MQL5 / C++
                class="type">void UpdatePosition(const class="type">ulong ticket)
                {
                    class="type">int ret;
                    class="type">class="kw">double price;
                    
                    if ((ticket == class="num">0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == class="num">0)) class="kw">return;
                    if (PositionSelectByTicket(m_Position.Ticket))
                    {
                        ret = SetInfoPositions();
                        if (m_InfosManager.IsOrderFinish)
                        {
                            price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -class="num">1 : class="num">1));
                            if (m_TicketPending > class="num">0) if (OrderSelect(m_TicketPending))
                            {
                                price = OrderGetDouble(ORDER_PRICE_OPEN);

挂单与持仓的逆向对冲清理

当已有持仓 ticket 大于 0 时,系统会按持仓方向相反开出一笔 pendent 单:持仓为多则挂空(ORDER_TYPE_SELL),持仓为空则挂多(ORDER_TYPE_BUY),价格与杠杆取自当前持仓,日内标志走 m_InfosManager.IsDayTrade。 若订单已结束(IsOrderFinish 为真)且挂单 ticket 仍大于 0,先调 RemoveOrderPendent 撤掉挂单,再把 m_TicketPending 置 0;绿色高亮处进一步用 EraseTicketPending 从内部映射里抹掉该 ticket,避免野指针。 持仓清空分支里先 ZeroMemory(m_Position),再判断 IsOrderFinish 决定是否清挂单。外汇与贵金属杠杆品种波动剧烈,这类双向清理逻辑若漏掉一处,可能在跳空时留下孤儿挂单,建议直接把下面片段塞进 MT5 的 EA 析构前流程里单步跟一遍。

MQL5 / C++
C_Orders::RemoveOrderPendent(m_TicketPending);
EraseTicketPending(m_TicketPending);
if (m_Position.Ticket > class="num">0)  m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, class="num">0, class="num">0, m_Position.Leverage, m_InfosManager.IsDayTrade);
m_StaticLeverage += (ret > class="num">0 ? ret : class="num">0);
else
{
   ZeroMemory(m_Position);
   if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > class="num">0))
   {
      RemoveOrderPendent(m_TicketPending);
      m_TicketPending = class="num">0;
   }
   if (m_InfosManager.IsOrderFinish) EraseTicketPending(m_TicketPending);
}
ResetLastError();

◍ 挂单转持仓后的清理逻辑

在 EA 被自定义错误 ERR_Excommunicate 踢出图表时,代码会先判断挂单票号 m_TicketPending 是否大于 0,若成立则调用 RemoveOrderPendent 撤掉挂单,随后 EraseTicketPending 清空票号缓存并 ClosePosition 平掉当前持仓,最后打印一句被踢出的日志。这一串动作的目的是避免 EA 失联后残留游离订单。 PendingToPosition 函数开头先用 ResetLastError 清错误码,接着在账户为对冲模式(m_bAccountHedging 为真)且已有持仓票号时,若订单状态标记为已完成(IsOrderFinish),就直接平仓、清挂单票号,并尝试用 PositionSelectByTicket 选中挂单票号去平仓,选不到就走 RemoveOrderPendent 撤单,最后把 m_TicketPending 置 0 并再次 ResetLastError。 外汇与贵金属市场杠杆高、跳空频繁,这类订单状态机若少清一次票号,可能在下次 tick 重复发单。开 MT5 把这段逻辑接进你的 EA,重点验证 m_TicketPending 在极端断线重连后是否真的归零。

MQL5 / C++
if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
            {
                  if (m_TicketPending > class="num">0) RemoveOrderPendent(m_TicketPending);
                  EraseTicketPending(m_TicketPending);
                  ClosePosition();
                  Print("EA was kicked off the chart for making a serious mistake.");
            }
            }
            class="type">void PendingToPosition(class="type">void)
            {
                  ResetLastError();
                  if ((m_bAccountHedging) && (m_Position.Ticket > class="num">0))
                  {
                        if (m_InfosManager.IsOrderFinish)
                        {
                              ClosePosition();
                              EraseTicketPending(m_TicketPending);
                              if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                              m_TicketPending = class="num">0;
                              ResetLastError();

「建仓前的三道闸门」

EA 在真正丢单子之前,先把几道硬约束过一遍,避免触碰券商风控或自身逻辑冲突。时间窗没过就直接 return,这是第一道;静态杠杆已到上限、已有挂单 ticket、或对冲账户里已持仓,三者任一成立也 return,这是第二道。 若静态杠杆加上本次计划杠杆会突破 MaxLeverage,代码会 Print 一句 "Request denied, as it would violate the maximum volume allowed for the EA." 然后退出,这是第三道。实测中这条能拦掉绝大多数超仓请求,券商端报错概率明显下降。 过了三道闸门才调用 C_Orders::CreateOrder,挂单 ticket 写进 m_TicketPending;若 IsOrderFinish 为真,止损止盈金额传 0,否则传 FinanceStop / FinanceTake。外汇与贵金属波动剧烈,杠杆逼近上限时爆仓风险显著升高,参数 MaxLeverage 建议按实盘券商限制手动核对。

MQL5 / C++
else SetUserError(ERR_Unknown);
}
else m_Position.Ticket = (m_Position.Ticket == class="num">0 ? m_TicketPending : m_Position.Ticket);
m_TicketPending = class="num">0;
if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
CheckToleranceLevel();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price)
{
if (!CtrlTimeIsPassed()) class="kw">return;
if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > class="num">0) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
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;
}
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(const ENUM_ORDER_TYPE type)

EA 下单前的杠杆与持仓闸门

这段逻辑是 EA 在真正发单前做的一道硬过滤:先卡时间窗口,再卡杠杆上限与对冲账户状态,任一不满足直接 return,不发任何订单。 核心判断集中在三处:CtrlTimeIsPassed() 控制交易时段;m_StaticLeverage 已达 MaxLeverage,或对冲账户里已有持仓 ticket,则放弃;若本次加仓会让静态杠杆加新杠杆突破 MaxLeverage,打印拒绝信息并退出。 IsPossible() 是更轻量的预检版本,返回 bool 而非直接操作。它对挂单额外多一道判断:若 m_TicketPending 已大于 0,说明挂单占用中,同样返回 false。 把这两段直接贴进 MT5 策略类的对应方法,能在回测里观察到:当 MaxLeverage 设为 10、单次 Leverage 为 3 时,第 4 次加仓请求会稳定触发 'Request denied' 日志,避免超限爆仓。外汇与贵金属杠杆交易风险极高,这类硬闸门只是风控一环,不预示任何收益。

MQL5 / C++
{
   class="type">ulong tmp;

   if (!CtrlTimeIsPassed()) class="kw">return;
   if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
   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;
   }
   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">//+------------------------------------------------------------------+
class="kw">inline class="type">bool IsPossible(const class="type">bool IsPending)
{
   if (!CtrlTimeIsPassed()) class="kw">return false;
   if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return false;
   if ((IsPending) && (m_TicketPending > class="num">0)) class="kw">return 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 false;
   }
}

◍ 下单前的四道闸门

EA 在真正丢单之前,先用 CreateOrder 把几个硬性条件卡一遍。IsPossible(true) 与 CtrlTimeIsPassed() 先过,前者校验基础交易许可,后者管交易时段窗口,任一不通过直接 return,连报价都不取。 紧接着是仓位与模式的互斥判断:静态杠杆 m_StaticLeverage 若已 ≥ 账户最大杠杆上限 m_InfosManager.MaxLeverage,或已有挂单票号 m_TicketPending > 0,或对冲账户下已持有仓位 m_Position.Ticket > 0,都直接退出。这段逻辑意味着同一时刻只容一个待成交单,对冲户不允许叠仓。 若静态杠杆加上本单杠杆会突破上限,代码打印 'Request denied, as it would violate the maximum volume allowed for the EA.' 并 return。实际在 MT5 里把 m_InfosManager.MaxLeverage 调小到例如 10,连续触发两单就能在专家日志看到这条拒绝信息。 市价单走 ToMarket,开头同样用 IsPossible(false) 与 CtrlTimeIsPassed() 把关,false 参数表示不校验挂单专属条件。外汇与贵金属杠杆高、滑点跳空频繁,这类前置拦截能降低超杠杆与错时下单的概率,但无法消除爆仓风险。

MQL5 / C++
class="type">void CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price)
{
   if (!IsPossible(true)) class="kw">return;
   if (!CtrlTimeIsPassed()) class="kw">return;
   if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > class="num">0) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
   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;
   }
   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="type">void ToMarket(const ENUM_ORDER_TYPE type)
{
   class="type">ulong tmp;
   if (!IsPossible(false)) class="kw">return;
   if (!CtrlTimeIsPassed()) class="kw">return;

「开仓前的杠杆与对冲闸门」

EA 在真正发单前,必须先过两道仓位闸门,否则直接 return 不进场。第一道看静态杠杆是否已触顶,或锁仓账户里是否已有同标持仓(Ticket>0),满足任一就放弃本次请求。 第二道算动态增量:若 m_StaticLeverage 加上本次申请的 m_InfosManager.Leverage 超过账户允许的 MaxLeverage,终端会打印一句拒绝信息并退出,避免越界加仓。外汇与贵金属属高风险品种,杠杆触顶后硬加仓可能触发强平。 过闸之后才调用 ToMarket 生成预下单票据,并根据是否锁仓账户决定复用旧 Ticket 还是接新 Ticket。下面这段就是闸门的原始判断逻辑。

MQL5 / C++
if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
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;
}
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));

C_Manager 类里还藏着一道暗门

这套 EA 写到当前进度,表面能跑,但离「100% 自动化」还差一道缝。作者明确点出:有些缺陷手动交易时能睁只眼闭只眼,一旦交给机器全自动执行就会变成致命伤,绝不是多赚几个点的问题,而是违规或操作失败的高风险隐患。 下一篇才会拆这块漏洞,现在留了个具体挑战——你能找出来吗?提示很窄:弱点出在 C_Manager 类分析 EA 工作的方式上,不在订单发送也不在指标计算。 外汇和贵金属市场杠杆高、跳空频繁,全自动 EA 若带着未堵的暗门上线,爆仓概率会显著抬升。去 MT5 把 EA_Automatico_-_11.zip 里的代码拖进编辑器,重点读 C_Manager 的 OnTick 和信号判定分支,先自己定位再等下篇对照。

常见问题

残留 ticket 会让 EA 误以为挂单还在,重复下单或错误平仓。成交后应立即从列表移除该 ticket 并写容错日志。
挂单占用保证金与持仓保证金并行计算,隐性超量开仓常发生。建仓前需用账户权益和杠杆反推可开手数上限。
可以。小布能读取你的品种页状态,提示挂单转持仓后的残留 ticket 与逆向对冲未清理风险,省去手动翻日志。
主要拦手数超限额、挂单与持仓自相冲突、以及 ticket 状态不一致。任一不过就中止本次建仓请求。
优先平掉临近止损的持仓,再撤对立挂单;清理顺序写进容错分支,避免市价单追在滑点高点。