构建自动运行的 EA(第 11 部分):自动化(III)·进阶篇
(2/3)· 从交互点剥离到自动触发,90% 的人栽在忍不住改旧代码这一步
很多交易者在 EA 临近全自动时手痒去动已经跑稳的旧逻辑,结果触发连锁崩溃。真正成熟的自动化不是加功能,而是把人和代码之间的接口逐个拆掉。本篇接上篇的操作计划控制,继续把系统推向无需看管的最后阶段。
「挂单成交后如何接管保本逻辑」
在 EA 里处理挂单变持仓的过渡期,最容易漏掉的是:挂单未成交时不该触发保本,成交后才该把保护价算进去。下面这段逻辑先用 m_TicketPending 判断是否还有挂单号,再用 OrderSelect 抓挂单开盘价 v1,和当前价 v2 比大小来决定 EnableBreakEven 的开合。 如果走的是 def_ORDER_FINISH 宏分支,条件是订单结束且挂单号为 0;若换成运行期对象 m_InfosManager.IsOrderFinish,写法一致但把编译期开关变成了实时状态。多空方向用 m_Position.IsBuy 区分:买仓要求 v1 < v2 才开保本,卖仓反之。 TriggerBreakeven 函数则是另一层触发:用 PositionSelectByTicket 锁定持仓,读 POSITION_PROFIT,当浮盈 >= m_Trigger(通常以金额计)时,把保护价推到开仓价加一个 PointPerTick 的偏移。买仓加、卖仓减,正好贴着成本线。外汇与贵金属杠杆高,这类保本若 PointPerTick 设太小,遇滑点可能被扫在成本外侧,建议开 MT5 用策略测试器打印 m_Trigger 与 Gap 的实际点数核对。
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)); if (m_InfosManager.IsOrderFinish) if (m_TicketPending > class="num">0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN); m_Position.EnableBreakEven = (m_InfosManager.IsOrderFinish ? 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; 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_InfosManager.IsOrderFinish) {
◍ 挂单与市价单的杠杆闸门
这段逻辑里最硬的一条是:只要静态杠杆达到上限、或已有挂单 ticket、或对冲账户里已持仓,函数直接 return 不发单。把 def_MAX_LEVERAGE 换成经纪商返回的 m_InfosManager.MaxLeverage,就是实盘里防止超额杠杆的真实护栏。
CreateOrder 里两行高亮其实是同一段逻辑的迭代:旧版用宏常量判断,新版改用账户管理器的动态字段。发单时若 IsOrderFinish 为真,止损止盈金额传 0,否则取管理器里的 FinanceStop / FinanceTake,杠杆与日内标记也由管理器统一下发。
ToMarket 和 CreateOrder 都先跑 CtrlTimeIsPassed(),时间窗没过一律不动作。外汇与贵金属波动剧烈、杠杆风险高,这类前置拦截能避免在非交易时段或超额状态下误触单。
下面把关键几行拆开看:
if (m_TicketPending > 0) m_Position.EnableBreakEven = !ModifyPricePoints(m_TicketPending, price, 0, 0);
// 若有挂单,就对该挂单调 ModifyPricePoints 改价点,并把保本开关取反
else m_Position.EnableBreakEven = !ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
// 否则对当前持仓改开仓价/止盈,同样反转保本启用状态
| if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) | (m_TicketPending > 0) | (m_bAccountHedging && (m_Position.Ticket > 0))) return; |
|---|
// 杠杆到顶、有挂单、或对冲户已持仓,三种情况全拦截不发单 m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade); // 真正发单:止损失败金额按结束标志决定,杠杆与日交标记来自管理器
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="type">void CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price) { if (!CtrlTimeIsPassed()) class="kw">return; if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > class="num">0) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) 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; 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); 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 (!CtrlTimeIsPassed()) class="kw">return; if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
挂单转持仓与持仓更新的短路逻辑
在 EA 的订单管理类里,开仓前先卡两道门槛:静态杠杆已顶到账户允许的最大值,或者对锁账户下已经持有一张票号大于 0 的仓位,就直接 return 不再发单。这两行判断能避免在高杠杆限制下硬挤订单,也防止对锁模式重复开反向仓。 发单时调用 C_Orders::ToMarket,传入订单类型、止损金额、止盈金额、杠杆与是否日内交易标记。注意旧写法里的 def_ORDER_FINISH 宏已被替换为 m_InfosManager.IsOrderFinish 布尔字段——若订单结束标志为真,止损止盈强制传 0,否则用管理器里的 FinanceStop / FinanceTake。 持仓票号赋值也分账户模式:对锁账户直接采用返回值 tmp;非对锁则仅在当前无仓位(Ticket<=0)时才写入 tmp,已有仓位就保留原票号。这样净仓模式不会覆盖在跑的仓位。 PendingToPosition 与 UpdatePosition 都先做防御:前者在对锁且有仓位时检查结束标志,后者严格要求传入 ticket 等于当前 m_Position.Ticket 且非零,否则直接 return。UpdatePosition 里用 PositionSelectByTicket 按票号回选持仓,选不中就不会往下走。外汇与贵金属杠杆可调,这类逻辑出错可能瞬间放大回撤,上 MT5 用策略测试器单步跟一遍最稳妥。
if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) 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="type">void PendingToPosition(class="type">void) { ResetLastError(); if ((m_bAccountHedging) && (m_Position.Ticket > class="num">0)) { if (m_InfosManager.IsOrderFinish) { class=class="str">"cmt">// ... The rest of the code ... 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)) {
「挂单反转的触发与价格重算」
这段逻辑处理的是订单结束后的反向挂单生成。当信息管理器判定当前订单已结束(IsOrderFinish 为真),系统先按持仓开仓价叠加止损点数重算挂单价:买入持仓反向挂卖单时价格向下偏移,卖出持仓反向挂买单时向上偏移,偏移量由 FinanceToPoints 按杠杆换算。 若此前已有挂单 ticket(m_TicketPending>0),代码会先 OrderSelect 选中它,读取其 ORDER_PRICE_OPEN 作为新挂单价,随后调用 RemoveOrderPendent 撤掉旧挂单。这一步保证反转价以实际挂单开盘价为准,而不是理论止损位。 撤单完成后,若原持仓 ticket 有效,CreateOrder 按反向方向下新挂单,手数传 0 表示沿用策略默认,IsDayTrade 标记是否日内。最后 m_StaticLeverage 累加 SetInfoPositions 的返回值(为负则记 0)。外汇与贵金属杠杆双向反转风险极高,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 1 小时数据跑一遍观察滑点。
ret = SetInfoPositions(); if (def_ORDER_FINISH) 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); C_Orders::RemoveOrderPendent(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 ((def_ORDER_FINISH) && (m_TicketPending > class="num">0))
◍ 挂单清理与移动止损的退出判定
EA 在订单生命周期末尾需要同时处理挂单回收与移动止损触发,核心是先确认订单状态再决定动作。下面这段逻辑里,若订单已完成且挂单票号大于 0,就移除挂单并把票号清零,避免残留 pending 干扰下一轮。 if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0)) { RemoveOrderPendent(m_TicketPending); m_TicketPending = 0; } 移动止损函数 TriggerTrailingStop 开头有一道硬门禁:当无持仓票号,或按订单完成标志判定挂单已清、又或止损价为 0 时直接 return。这意味着若启用 break-even 就走保本逻辑,否则才去取实时报价算 trailing。 取价时用了 SymbolInfoDouble,根据图表模式选 SYMBOL_LAST 还是买卖盘:持仓为多时取 SYMBOL_ASK,为空取 SYMBOL_BID。外汇与贵金属杠杆高,SL 逻辑写错可能瞬间放大回撤,建议把这段代码丢进 MT5 策略测试器,用 2023 年 XAUUSD 的 M15 数据跑一遍看 IsOrderFinish 分支是否如预期清零。
if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > class="num">0)) { RemoveOrderPendent(m_TicketPending); m_TicketPending = class="num">0; } class="kw">inline class="type">void TriggerTrailingStop(class="type">void) { class="type">class="kw">double price, v1; if ((m_Position.Ticket == class="num">0) || (m_InfosManager.IsOrderFinish ? 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; if (m_InfosManager.IsOrderFinish)
挂单与持仓的价位点重算逻辑
这段实现里,先通过 OrderSelect 锁定挂单并读出其开仓价 v1,只有当 v1 大于 0 且当前 price 与 v1 的绝对差不小于 m_Position.Gap 的两倍时,才触发重定价。 重算时 price 被改写为 v1 加上方向修正量:买入方向加一个 Gap,卖出方向减一个 Gap,这个偏移量直接由 m_Position.IsBuy 的三元判断决定。 随后根据订单状态分流——若 IsOrderFinish 为真,就对挂单本身调 ModifyPricePoints 改价;否则改的是已开仓位的止损挂单价,TP 维持原值不变。 ClosePosition 内做了 Ticket 有效性判断,调用底层平仓后立刻 ZeroMemory 清空票据号,避免析构时重复操作。 析构函数里捕获了自定义错误码 ERR_USER_ERROR_FIRST + ERR_Excommunicate,一旦命中就强制移除挂单并平掉持仓,属于退出时的清理兜底。外汇与贵金属杠杆品种波动剧烈,这类自动清理若 Gap 参数设得过小,可能在秒级滑点中误触平仓,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍验证。
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) if (m_InfosManager.IsOrderFinish) ModifyPricePoints(m_TicketPending, price, class="num">0, class="num">0); else ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP); } } class="kw">inline class="type">void ClosePosition(class="type">void) { if (m_Position.Ticket > class="num">0) { C_Orders::ClosePosition(m_Position.Ticket); ZeroMemory(m_Position.Ticket); } } ~C_Manager() { if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate)) { if (m_TicketPending > class="num">0) RemoveOrderPendent(m_TicketPending); if (m_Position.Ticket > class="num">0) ClosePosition(m_Position.Ticket);