构建自动运行的 EA(第 07 部分):账户类型(II)·进阶篇
◍ 净持结算账户上 EA 的隐形平均价陷阱
净持结算账户与对冲账户机制不同:当品种出价变动,交易服务器会自动重算多头或空头持仓的平均开仓价,同时持仓量也随之调整。若 EA 以「平掉部分仓位再开新仓」的方式滚动头寸,平均价就会被持续推动;这不是 MT5 或券商的锅,漏洞在 EA 自身的逻辑缺口。 自动 EA 在无人值守时最容易暴雷。它发出开仓指令后,可能因代码缺陷陷入「挂单滞留—不断发新请求改平均价」的循环,而终端显示的持仓手数没变,只是价格悄悄漂移。很多用户直到浮亏已经扩大才察觉,极端情况下几秒到几分钟就能吃掉大量本金。外汇与贵金属杠杆高,这类隐性磨损风险被放大。 部分 EA 是故意用这种均价滚动策略,但更多情况是编程者没意识到bug。某些触发器在特殊行情(如急速跳空)下会诱使 EA 疯狂买卖,若你没预判到这种行为,盯盘时会被吓到。别迷信任何系统的安全性,EA 永远可能藏着你写的时候没想到的裂缝。 缓解办法不算复杂:在 EA 代码里加持仓校验与强制定时退出逻辑,至少能把损害圈住。下一节我们直接看这段防护怎么写。
「用静态配额卡住 EA 的加仓手数」
限制 EA 风险最直白的办法,不是缩小单笔仓位,而是给整个运行周期能用的总交易量设一道硬顶。比如把最大允许交易量定为最小交易量的 10 倍(def_MAX_LEVERAGE = 10),EA 可以照常开仓,但累计手数触及这个倍数后就不再加仓,单笔仓位是 0.01 还是 0.5 都无所谓,配额吃完就停。 这道硬顶必须靠类里的静态私密全局变量来记,因为 MT5 在触发 DeInit 事件时会自动重置 EA,非静态数据全丢,只有静态变量能跨重置保留。但要注意:如果从图表上手动删除 EA 再加载,连静态变量也会清零,那种情况只能落盘到文件再读回。 下面这段是核心拦截逻辑:m_StaticLeverage 记录已用倍数,CreateOrder 里先判是否 >= 10 倍,或者已有挂单、对冲账户已有持仓,满足任一就直接 return 不开单。 别把静态变量当保险箱 MT5 自动重启 EA(DeInit 触发)能保住静态变量,但人为删图重加就归零。真要防灾难,得在 C_Manager 外把配额写文件,重启时读回,否则交易者重开图表那一刻锁定就失效,爆仓风险转回自己头上。 光有开仓拦截还不够,平仓和删除动作也要回滚静态计数。文末两个例程处理了持仓平掉后清内存、无持仓误调则回滚,但它们只是地基——面对多 EA 同品种对冲账户里某个‘坏’EA 被踢出后留一地挂单,还得在下篇接 EA 主体代码才能闭环。外汇和贵金属杠杆高,这套配额机制只是降低概率,不替你兜底。
class="kw">static class="type">uint m_StaticLeverage; class="type">uint C_Manager::m_StaticLeverage = class="num">0; class="macro">#define def_MAX_LEVERAGE class="num">10 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateOrder(const ENUM_ORDER_TYPE type, 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, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ToMarket(const ENUM_ORDER_TYPE type) { class="type">ulong tmp;
锁仓账户下的下单与持仓同步逻辑
在 MT5 的 EA 封装里,遇到锁仓账户(m_bAccountHedging 为真)且已有持仓票号时,开仓函数会直接 return,避免同方向重复建仓导致对冲混乱。 下面这段是开仓分支里的关键拦截与票号接管:
| if ((m_StaticLeverage >= def_MAX_LEVERAGE) | (m_bAccountHedging && (m_Position.Ticket > 0))) return; |
|---|
tmp = C_Orders::ToMarket(type, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade); m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp)); if (PositionSelectByTicket(m_Position.Ticket)) SetInfoPositions(); else m_Position.Ticket = 0; UpdatePosition 处理挂单转持仓后的票号交接:若 bSwap 为真且处于锁仓模式,原持仓票号大于 0 会主动抛 ERR_Unknown 用户错误,防止旧仓位被误覆盖。 m_Position.Ticket = ((m_Position.Ticket == 0) && (bSwap) ? m_TicketPending : m_Position.Ticket); m_TicketPending = (bSwap ? 0 : m_TicketPending); if (PositionSelectByTicket(m_Position.Ticket)) { ret = SetInfoPositions(); m_StaticLeverage += (ret > 0 ? ret : 0); } RemovePosition 则先做空票号守卫:m_Position.Ticket == 0 时直接退出,省掉一次无谓的持仓对象清理。外汇与贵金属杠杆高,这类票号状态机写错,可能在一秒内开出预期外的对冲单。
if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return; tmp = C_Orders::ToMarket(type, m_InfosManager.FinanceStop, 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)); if (PositionSelectByTicket(m_Position.Ticket)) SetInfoPositions(); else m_Position.Ticket = class="num">0; class="kw">inline class="type">void UpdatePosition(const class="type">bool bSwap = false) { class="type">int ret; if ((m_bAccountHedging) && (m_Position.Ticket > class="num">0) && (bSwap)) SetUserError(ERR_Unknown); m_Position.Ticket = ((m_Position.Ticket == class="num">0) && (bSwap) ? m_TicketPending : m_Position.Ticket); m_TicketPending = (bSwap ? class="num">0 : m_TicketPending); if (PositionSelectByTicket(m_Position.Ticket)) { ret = SetInfoPositions(); m_StaticLeverage += (ret > class="num">0 ? ret : class="num">0); } } class="kw">inline class="type">void RemovePosition(class="type">void) { if (m_Position.Ticket == class="num">0) class="kw">return; }
◍ 平仓后的内存清理动作
上面这段片段只做了一件事:在通过持仓单号选中对应仓位后,立即执行平仓,并把本地持仓结构体清空。 PositionSelectByTicket 依据 ticket 在交易池里定位持仓,返回 true 才说明当前上下文拿到了这张单子;ClosePosition 传入同一 ticket 触发市价平仓。外汇与贵金属保证金交易波动剧烈,平仓指令可能因点差跳空而部分成交,实际成交价与预期存在偏差属正常概率。 ZeroMemory(m_Position) 把结构体字段归零,避免下一轮逻辑误用上一笔持仓的残留数据。开 MT5 把这两行塞进你的持仓管理函数里,跑一遍就能确认ticket失效时是否跳过平仓而不报错。
if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket); ZeroMemory(m_Position);