构建自动运行的 EA(第 11 部分):自动化(III)·综合运用
挂单转持仓时的对冲平仓分支
在开启账户对冲模式(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 策略测试器用真实点差回测验证。
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 在撤单后是否准时归零,比读十遍文档管用。
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 是重构后私有区的真实代码,负责按持仓票更新挂单价格: 别把重复代码当无害 三处几乎一样的发单片段,用宏或函数收口都行;选函数是因为读者多刚入门,函数比宏更易单步跟踪。聚合后所有划掉的行直接删,程序更稳,也少一处未来改漏的地方。
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 析构前流程里单步跟一遍。
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 在极端断线重连后是否真的归零。
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 建议按实盘券商限制手动核对。
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' 日志,避免超限爆仓。外汇与贵金属杠杆交易风险极高,这类硬闸门只是风控一环,不预示任何收益。
{
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 参数表示不校验挂单专属条件。外汇与贵金属杠杆高、滑点跳空频繁,这类前置拦截能降低超杠杆与错时下单的概率,但无法消除爆仓风险。
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。下面这段就是闸门的原始判断逻辑。
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 和信号判定分支,先自己定位再等下篇对照。