构建自动运行的 EA(第 09 部分):自动化(I)·综合运用
🤖

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

(3/3)· 从 OCO 订单到盈亏平衡与尾随停止,拆解一套适配净持结算账户的低负载自动化机制

案例拆解 第 3/3 篇

很多交易者把自动化想成无限挂单和密集轮询,结果 EA 越写越卡,平台资源被吃满。其实在 MetaTrader 5 里,用轻量事件配合持仓级触发,就能绕开这种超载陷阱。本篇接上两篇,把前面铺垫的类与函数真正跑成一套可用的自动化。

挂单清理与仓位信息的实时同步

这段逻辑处理的是 EA 在每轮 tick 里对挂单和当前持仓的兜底同步。当分支走到 else 时,说明前置条件未满足,先 ZeroMemory(m_Position) 把结构体清零,避免上一轮残留数据污染本次判断;若宏 def_ORDER_FINISH 为真,则调用 RemoveOrderPendent 撤掉未成交挂单并把 m_TicketPending 置 0,随后 ResetLastError 清掉可能累积的错误码。 SetInfoPositions 负责把 MT5 持仓池里的关键字段搬进自定义结构。杠杆倍数用持仓量除以终端最小交易量算出来:m_Position.Leverage = (int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal),例如 VolMinimal 为 0.01 手时,0.1 手持仓会得出 Leverage = 10。方向、TP、SL、开仓价都从 PositionGetInteger / PositionGetDouble 取,若挂单未完成且选中成功,则用挂单开仓价 v1 覆盖 SL 参考值。 EnableBreakEven 的判定值得细看:若 def_ORDER_FINISH 且挂单已清(m_TicketPending==0),或按买卖方向判断 v1 与 v2(开仓价)的位置关系,买权时 v1<v2、卖权时 v1>v2 才允许保本触发。最后 Gap 由 FinanceToPoints 把触发金额转成点数,函数返回本次杠杆与旧值的差,供上层感知仓位变动。 TriggerBreakeven 函数体在这里只声明了 double price,具体移动 SL 到 break-even 的算法在后续片段。外汇与贵金属杠杆品种波动剧烈,这类自动保本逻辑可能在某些滑点行情下失效,实盘前建议在 MT5 策略测试器用历史数据跑一遍。

MQL5 / C++
else
{
ZeroMemory(m_Position);
		if (def_ORDER_FINISH)
{
RemoveOrderPendent(m_TicketPending);
m_TicketPending = class="num">0;
}
}
			ResetLastError();
		}
class="kw">inline class="type">int SetInfoPositions(class="type">void)
		{
			class="type">class="kw">double v1, v2;
			class="type">int tmp = m_Position.Leverage;
			
			m_Position.Leverage = (class="type">int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal);
			m_Position.IsBuy = ((class="type">ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY;
			m_Position.TP = PositionGetDouble(POSITION_TP);
			v1 = m_Position.SL = PositionGetDouble(POSITION_SL);
			v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
			if (def_ORDER_FINISH) if (m_TicketPending > class="num">0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
			m_Position.EnableBreakEven = (def_ORDER_FINISH ? m_TicketPending == class="num">0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
			m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);
			class="kw">return m_Position.Leverage - tmp;
		}
class="kw">inline class="type">void TriggerBreakeven(class="type">void)
		{
			class="type">class="kw">double price;
			

◍ 保本与跟踪止损的触发逻辑

这段片段展示了在持仓盈利达到触发阈值后,把止损挪到开仓价上方(多单)或下方(空单)一个 tick 距离的实现路径。先通过 PositionSelectByTicket 锁定持仓,再用 PositionGetDouble(POSITION_PROFIT) 对比 m_Trigger,满足条件才计算保本价。 保本价计算用到了开仓价加/减 PointPerTick:多单加、空单减,对应 m_Position.IsBuy 的三元判断。若挂单已成交(def_ORDER_FINISH 为真)就改挂单的 ModifyPricePoints,否则直接改原持仓的 SL 到开仓价、新 SL 到保本价。 TriggerTrailingStop 负责总调度:Ticket 为 0 或对应单子 SL 为 0 时直接 return;若已启用保本(EnableBreakEven)就调 TriggerBreakeven,否则读 SymbolInfoDouble 取当前市场价(LAST / ASK / BID 视品种和图表模式而定)准备跟踪。外汇与贵金属波动剧烈,这类逻辑在实盘里可能频繁改单,滑点风险需自行在 MT5 回测验证。

MQL5 / C++
if (PositionSelectByTicket(m_Position.Ticket))
  if (PositionGetDouble(POSITION_PROFIT) >= m_Trigger)
  {
    price = m_Position.PriceOpen + (GetTerminalInfos().PointPerTick * (m_Position.IsBuy ? class="num">1 : -class="num">1));
    if (def_ORDER_FINISH)
    {
      if (m_TicketPending > class="num">0) m_Position.EnableBreakEven = !ModifyPricePoints(m_TicketPending, price, class="num">0, class="num">0);
    }else m_Position.EnableBreakEven = !ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
  }
}
class="kw">inline class="type">void TriggerTrailingStop(class="type">void)
  {
    class="type">class="kw">double price, v1;

    if ((m_Position.Ticket == class="num">0) || (def_ORDER_FINISH ? m_TicketPending == class="num">0 : m_Position.SL == class="num">0)) class="kw">return;
    if (m_Position.EnableBreakEven) TriggerBreakeven(); else
    {
      price = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : (m_Position.IsBuy ? SYMBOL_ASK : SYMBOL_BID)));
      v1 = m_Position.SL;

「挂单成交后如何重定位加仓价」

当挂单已成交(def_ORDER_FINISH 为真),脚本先通过 OrderSelect 锁定原挂单票号,再用 OrderGetDouble 读出它的实际开盘价 v1。这一步很关键:你之后加仓的参考点不能是预设价,而是真实成交价。 若 v1 大于 0,且当前 price 与 v1 的绝对差不小于 m_Position.Gap 的两倍,说明价格已脱离原挂单位置足够远,具备重定价条件。此时按方向重写 price:买入侧加一个 Gap,卖出侧减一个 Gap。 重定价后分两条路走:挂单类调用 ModifyPricePoints 改挂单价;持仓类则把原持仓的开盘价、新 price、原 TP 一并传入修改。外汇与贵金属杠杆高,这种自动重定位若 Gap 设太小,可能在震荡里频繁改单、放大滑点风险。 把下面这段直接贴进 MT5 的 EA 调试,把 m_Position.Gap 从 10 点调到 20 点,观察 ModifyPricePoints 的触发次数变化,比看文字直观得多。

MQL5 / C++
if (def_ORDER_FINISH) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
if (v1 > class="num">0) if (MathAbs(price - v1) >= (m_Position.Gap * class="num">2))
{
   price = v1 + (m_Position.Gap * (m_Position.IsBuy ? class="num">1 : -class="num">1));
   if (def_ORDER_FINISH) ModifyPricePoints(m_TicketPending, price, class="num">0, class="num">0);
   else ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
}

从模拟账户跑通双指令防跳空

前面几节把基于止损线的最简触发逻辑讲完了,附件里的 EA 初始只带止损线,要用挂单当止损得照着改。若想把它塞进投资组合里自动跑,这套机制倾向不合适,更建议留作手动系统的补充。外汇和贵金属波动大、跳空频繁,这类品种用 OCO 单时止损可能被跳过。 有读者在讨论里问过:OCO 平仓后挂单是否一并撤销。实际在服务器侧,若开仓时就把止损止盈绑在单子上,只发一次请求,触发后整仓平掉;但价格跳空时 OCO 可能不成交。部分程序员改用两张指令控仓,账簿里留一个不被覆盖的“跛脚”单,即使市价跳过 OCO 也大概率能接住平仓。不过对冲账户允许同标的多空同开,这招在 HEDGING 账户上不生效,净额账户才会在第二张单触发时把仓平掉。 想真弄懂,别光看文字。开 MT5 建模拟账户,外汇用对冲、股票用净额,把附带的 EA_Automatico_-_09.zip(9.27 KB)拖进去跑一遍,观察跳空时段两张指令的成交差异,比读十遍描述都实在。

把订单状态巡检交给小布
这些持仓盈亏平衡与尾随停止的判定逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时诊断,你只需在策略层做取舍。

常见问题

本篇机制主要针对净持结算账户设计,对冲账户也可用,但需注意同品种多持仓时止盈止损的归属逻辑可能需额外过滤。
因为开仓价已确定,类构造时传入的触发利润值等价于价位偏移,比较 POSITION_PROFIT 后直接发价给订单类即可,避免过度计算。
可以,小布盯盘的品种页会解析持仓与挂单联动状态,若 OCO 取消未连带清理止损,会有提示,省去你翻日志。
若未先触发盈亏平衡,尾随函数可能把止损移到成本下方,造成提前损单,所以调用顺序应固定在盈亏平衡成功之后。
原文后续会扩展更可靠机制,当前篇先从简单 OCO 起步,波动极高时建议拉长触发阈值或降低轮询密度。