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

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

第 1/3 篇

先看清账户类型再写 EA

MT5 里跑 EA 前,账户类型是绕不开的前提。同一套下单逻辑,在净头寸账户(hedging 关闭)和锁仓账户(hedging 开启)上行为完全不同,写错判断条件会直接让 PositionSelect 返回错误结果。

  • 年 4 月 3 日发布的这篇系列第 7 部分原文阅读量 1754,说明很多实盘者卡在账户差异而不是策略本身。开 MT5 按 F2 看账户窗口的『对冲』字段,能立刻确认当前是哪种模式。

净头寸账户里同一品种只能有一条 position,平仓用 POSITION_TYPE 反手;锁仓账户允许同品种多空各一条。EA 初始化时用 ENUM_ACCOUNT_MODE 读 ACCOUNT_MARGIN_MODE 之外的属性,比运行时反复猜更稳。

◍ 净持结算账户带来的隐藏麻烦

上一节里我们搭好了 C_Manager 类,用来在 EA 行为异常时把它从图表强制移除,也顺手做了挂单和市价单的触发拦截。但这套机制还没覆盖完 EA 与 C_Orders 类之间的所有交互死角,净持结算账户就是其中一个典型坑。 在净持平掉(netting)账户下,同一品种只允许存在一个同向持仓,开仓逻辑和锁仓账户完全不同。如果 EA 仍按逐仓思路调用 C_Orders 的下单接口,就会出现平仓失败或误平不该平的头寸,这类问题不会立刻报错,却会让策略悄悄偏离原意。 外汇和贵金属杠杆高、滑点跳空频繁,自动 EA 绝不能在无人盯盘时裸跑。哪怕代码里写了再聪明的保护,也只是降低失控概率,不表示能替你兜底。发现订单动作超出预设边界,趁还没爆仓前手动拖出图表最稳妥。

「给 EA 调用下单类套上枷锁」

EA 若仍能随意穿透 C_Orders 类直接发单,前面做的所有管控逻辑都等于空谈。即便初始化阶段由管理类强制过规则,运行期若不下死命令限制,市价单和挂单仍可能被悄悄送进订单簿——这是实盘里最容易被忽略的越权漏洞。 我们在 CreateOrder 里先卡一道:示意挂单单号变量 m_TicketPending 大于 0,或对冲账户已存在持仓(m_Position.Ticket > 0),新挂单请求直接 return。条件放行后才把 C_Orders::CreateOrder 的返回值写进 m_TicketPending,此后同周期再调便因单号有效而被拒。 ToMarket 的坑更深。市价单只允许在对冲账户无持仓时发送;净持结算账户下,首次调用返回值可能与持仓单号不一致,若直接覆盖持仓单号会丢真实单号。所以对冲账户可放心存返回值,净持账户仅当 m_Position.Ticket 为 0 时才保存,否则忽略。外汇与贵金属杠杆高,这类账户类型差异处理不当可能直接引发重复开仓风险。 下面这段是加了拦截后的核心片段,绿底行即挂单拦截,黄底行即市价单拦截:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
								class="type">void CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price)
									{
										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)
									{
										C_Orders::ToMarket(type, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
									}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
								class="type">void CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price)
									{
										if ((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;
										
										if (m_bAccountHedging && (m_Position.Ticket > class="num">0)) class="kw">return;

对冲账户下的持仓票号接管逻辑

这段片段处理的是下单后持仓票号(Ticket)的回写,核心分歧在账户是否开启对冲模式。当 m_bAccountHedging 为真,每次市价单都直接采用 ToMarket 返回的新票号 tmp,不关心旧仓位。 非对冲账户走的是另一套:若原 m_Position.Ticket 已大于 0 就沿用旧票号,否则才用 tmp。这意味着净持仓账户不会因加仓而丢失原仓位引用,避免重复开仓统计错位。 随后用 PositionSelectByTicket 校验票号是否真实存在,成功则刷新持仓信息,失败就把 Ticket 置 0。外汇与贵金属保证金交易高风险,对冲开关直接影响仓位管理逻辑,建议在 MT5 策略测试器里切换账户类型验证该分支。

MQL5 / C++
            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;
            }

常见问题

两种账户的下单与持仓逻辑完全不同,净持账户同方向加仓会合并、反手会平仓,EA若按对冲逻辑写会直接算错仓位,先看清类型再动手。
同品种多单会自动合并成一条净头寸,EA用票号逐个管理持仓会失效,必须在开仓前用PositionSelect查净仓而非数订单。
小布可读取你当前账户属性并标注净持/对冲差异,提醒EA里该用Position类还是Order类,省去手动翻文档。
用PositionGetTicket逐个遍历持仓获取真实票号,再绑定到EA内部管理数组,避免新开仓与旧仓脱节。
把OrderSend包一层校验函数,先判账户类型再决定走净持平转或对冲开仓,非法请求直接拦截不发出。