构建自动运行的 EA(第 07 部分):账户类型(II)·进阶篇
📘

构建自动运行的 EA(第 07 部分):账户类型(II)·进阶篇

第 2/3 篇

◍ 净持结算账户上 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 主体代码才能闭环。外汇和贵金属杠杆高,这套配额机制只是降低概率,不替你兜底。

MQL5 / C++
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 时直接退出,省掉一次无谓的持仓对象清理。外汇与贵金属杠杆高,这类票号状态机写错,可能在一秒内开出预期外的对冲单。

MQL5 / C++
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失效时是否跳过平仓而不报错。

MQL5 / C++
if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket);
ZeroMemory(m_Position);

常见问题

净持账户会把同方向多空合并成一条净头寸,EA 读到的平均价是合并后的虚拟值,不是真实分批建仓成本。回测或实盘前应在代码里单独记录每笔开仓价,别直接信系统返回的平均价。
在初始化时定义一个固定手数上限常量,每次下新单前用 PositionTotal 累加已开手数做判断,超了就直接 return。这样不依赖动态计算,能卡住极端行情下的加仓冲动。
可以。小布能按你设定的品种和魔法码,自动比对 EA 应下单数与实际持仓,发现锁仓不同步就弹提醒,省得你半夜翻终端窗口。
很多 EA 只在开仓时写数组,平仓事件里忘了置空或删除对应元素。要在 OnTradeTransaction 的平仓分支里显式清理,否则下一轮循环会误判还有仓。
大概率是下单前没检查账户类型是否允许对冲,或止损价距超出品种限制被拒。先打印返回错误码确认,再按锁仓规则分开校验多空下单参数。