构建自动运行的 EA(第 09 部分):自动化(I)·进阶篇
⚙️

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

(2/3)· 从 OCO 订单的盈亏平衡与尾随停止起步,看 EA 如何不靠 OnTick 也能稳跑

案例拆解 第 2/3 篇
很多交易者把自动化等同于 OnTick 里堆逻辑,结果 EA 卡顿、报错、错过行情。其实 MetaTrader 5 里有一种更轻的机制,能在不压垮平台的前提下把初级自动化跑起来,尤其适合净持结算账户。

「别拿 OnTick 当触发器调用方」

在 MT5 里用 OnTick 去调尾随停止这类触发器,看着顺手实则危险。OnTick 在每次收到服务器跳价时都会触发,而在高频交易环境下,短短 1 毫秒内服务器可能推来 10 次以上这类事件。普通 EA 跑在离交易服务器几百公里外的机器上,延迟远高于服务器操作频率,根本拼不过机房里的机构机器人。 若让 OnTick 直接驱动检查逻辑,平台会被远超 CPU 处理能力的事件淹没,尤其当资产波动性快速放大时(指数期货常这样),崩溃概率显著上升。外汇与贵金属同样属高风险品种,这种卡死可能让你在关键时刻掉线漏单。 正确做法是改用 OnTime 事件。在 OnInit 里用 EventSetMillisecondTimer 设个 50~100 毫秒的短定时器,就能每秒约 10 次平稳验证触发器,不必依赖跳价密度。记住在 OnDeinit 调用 EventKillTimer 释放,是好习惯。下面这段代码把触发器从 OnTick 彻底抽离,只留空壳: //+------------------------------------------------------------------+ int OnInit() { manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04, user08); // 初始化管理器对象,传入魔数与各用户参数 mouse = new C_Mouse(user05, user06, user07, user03, user02, user01); // 初始化鼠标交互对象 (*manager).CheckToleranceLevel(); // 检查容差等级是否合规 EventSetMillisecondTimer(100); // 设定 100 毫秒毫秒级定时器,驱动 OnTime return INIT_SUCCEEDED; // 返回初始化成功 } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete manager; // 释放管理器内存 delete mouse; // 释放鼠标对象内存 EventKillTimer(); // 注销毫秒定时器,停止 OnTime 事件 } //+------------------------------------------------------------------+ void OnTick() { } // 故意留空,不挂载任何触发器逻辑 //+------------------------------------------------------------------+ void OnTimer() { (*manager).TriggerTrailingStop(); // 由定时器每 100ms 调用一次尾随停止检查 } //+------------------------------------------------------------------+ 把这段代码丢进 MT5 策略测试器,对比 OnTick 满负荷和 OnTime 100ms 两种模式,你会看到后者在波动剧增时 CPU 占用和事件队列都更可控。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
    manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04, user08);
    mouse = new C_Mouse(user05, user06, user07, user03, user02, user01);
    (*manager).CheckToleranceLevel();
    EventSetMillisecondTimer(class="num">100);
    class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
    class="kw">delete manager;
    class="kw">delete mouse;
    EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick() { }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
    (*manager).TriggerTrailingStop();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用挂单替代止损线的坑与改法

高波动行情下,OCO 订单系统可能直接跳过离场指令,这时候有人会想用挂单来兜底平仓。但这套做法在原对冲账户 EA 里跑不通——C_Manager 类会报错把 EA 踢出图表;净持结算账户上开反向单也没意义,更合理的动作是挂单触发后直接向服务器发两笔平仓请求,让对冲与净持行为一致。 挂单离场不是万灵药。挂单不一定在你设的价位成交,触发后按当时市价执行;EA 若严重故障、需要撤单平仓时,另一侧可能找不到交易对手。反过来,若连接丢失但挂单正常触发,同交易量反向挂单能平掉净持账户仓位,或在对冲账户上锁价,这是它唯一的硬优势。 代码层改动集中在 C_Manager。先加一个编译期开关 def_ORDER_FINISH,默认 false 走原 OCO 尾随+盈亏平衡;改成 true 就禁用止盈、改用挂单当止损。下为原文给出的片段,注意 def_MAX_LEVERAGE 限定杠杆上限 10,m_TicketPending>0 时不再发新单: 别手贱删 EA 自己下的挂单 挂单在这里等同止损支撑,你在 MT5 里手动删掉它,仓位就裸奔了。改完开关重编译前,图例 01 的订单/持仓点位窗口必须常开,盯着服务器上 EA 到底塞了什么单,别盲信。 盈亏平衡触发时,新逻辑让挂单偏移开盘价 1 个基点,保证至少 1 点利润。尾随停止函数里用三元运算符判断依据挂单票证还是止损价,再调挂单位置。整套系统不挑资产,用标的自身数据算停止线,但外汇/贵金属杠杆高,连错两次就够喝一壶,模拟账户跑通再上真钱。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MAX_LEVERAGE                class="num">10
class="macro">#define def_ORDER_FINISH                class="kw">false
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
                        class="type">void CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price)
                        {
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > class="num">0) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 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;

挂单转持仓的对冲账户处理逻辑

在 EA 的挂单转持仓流程里,账户是否开启对冲模式直接决定了 ticket 的归属方式。当 m_bAccountHedging 为真且已有持仓 ticket(m_Position.Ticket > 0)时,系统不会覆盖原持仓,而是走独立的平仓或报错分支。 若宏 def_ORDER_FINISH 开启,会先尝试 ClosePosition(m_Position.Ticket),成功则用 ZeroMemory 清空 ticket,随后无条件平掉挂单 m_TicketPending;若 def_ORDER_FINISH 为假,则调用 SetUserError(ERR_Unknown) 阻断流程。非对冲或原无持仓时,m_Position.Ticket 才从 m_TicketPending 接手,并把挂单号置 0。 最后一步值得盯:只要 _LastError != ERR_SUCCESS,就触发 UpdatePosition 重新校正持仓状态,再跑 CheckToleranceLevel()。外汇与贵金属杠杆品种波动剧烈,这套容错在高波动时段可能频繁触发,建议在 MT5 策略测试器用真实点差回测确认报错频率。

MQL5 / C++
if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
tmp = C_Orders::ToMarket(type, (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 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="type">void PendingToPosition(class="type">void)
  {
  ResetLastError();
  if ((m_bAccountHedging) && (m_Position.Ticket > class="num">0))
    {
    if (def_ORDER_FINISH)
      {
      if (ClosePosition(m_Position.Ticket)) ZeroMemory(m_Position.Ticket);
      ClosePosition(m_TicketPending);
      }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="type">void UpdatePosition(class="kw">const class="type">class="kw">ulong ticket)

「挂单反转的触发与价格计算」

当持仓被平台确认成交后,EA 并不会 passively 等待止损,而是立刻尝试挂一张反向单。逻辑入口先卡了两道闸:ticket 为 0,或当前 ticket 与持仓对象不一致就直接 return,避免对幽灵单操作。 通过 PositionSelectByTicket 锁定持仓后,调用 SetInfoPositions 取回风控参数;若命中 def_ORDER_FINISH 标志,说明该笔仓位已满足收尾条件,进入挂单分支。 反向挂单价用开仓价加减点差换算:price = 开仓价 + FinanceToPoints(止损金额, 杠杆) * (多单取 -1 / 空单取 +1)。若之前已有挂单 m_TicketPending>0,则改用原挂单的 ORDER_PRICE_OPEN 并先撤单,再以此价反向建单。 建单方向与原仓相反(多则挂卖、空则挂买),杠杆沿用持仓杠杆,是否日内由 m_InfosManager.IsDayTrade 决定。最后把 SetInfoPositions 的正返回值累加到 m_StaticLeverage,作为静态杠杆统计。外汇与贵金属波动剧烈,这类自动反转在高滑点时段可能偏离预期价,上 MT5 用策略测试器跑 EURUSD M15 复盘验证更稳妥。

MQL5 / C++
{
   class="type">int ret;
   class="type">class="kw">double price;
   
   if ((ticket == class="num">0) || (ticket != m_Position.Ticket)) class="kw">return;
   if (PositionSelectByTicket(m_Position.Ticket))
   {
      ret = SetInfoPositions();
      if (def_ORDER_FINISH)
      {
         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);
            C_Orders::RemoveOrderPendent(m_TicketPending);
         }
         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);
把挂单监控交给小布盯盘
这些 OCO 订单的盈亏平衡与尾随停止诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到持仓触发状态,你只管判断要不要手动干预。

常见问题

OCO 系统设计为当原订单或持仓被取消、平仓时,依附的止盈止损一同作废,避免残留单干扰后续策略。
机制都可用,但本篇主要针对净持结算账户设计,对冲账户需注意多仓并存时的触发逻辑差异。
函数先拉取持仓最新财会数值,与类初始化时设定的阈值比较,达到或超过即视为触发,再把开仓价送交订单类执行。
小布目前提供状态诊断与提醒,自动下单执行仍需在 EA 内完成,但你能少盯盘、把重复劳动交给小布。
OnTick 高频调用易令 EA 与平台负载过重,错过订单风险上升,后续小节会讲替代调用方式。