轻松快捷开发 MetaTrader 程序的函数库(第六部分):净持帐户事件(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第六部分):净持帐户事件(基础篇)

第 1/3 篇

「净持账户的事件处理从哪下手」

MetaTrader 5 同时支持对冲账户与净持结算账户,两者在持仓逻辑上差异明显:对冲账户允许同一品种多空单并存,净持账户同一品种只保留一条净头寸。开发自定义函数库时,若忽略账户类型,事件回调很容易误判仓位变化。 在净持账户上实现事件处理,核心是把“持仓更新”映射为“净头寸变更”而非“逐单增减”。2019 年 7 月 8 日发布的示例工程里,作者用一组轻量封装函数把账户类型判断前置,使后续 OnTradeTransaction 逻辑不必重复分支。 实测中,同一套事件处理代码在净持账户下触发次数明显少于对冲账户——因为开平对冲单的多次成交在净持端被合并为一次净仓变动。这种差异直接影响函数库的性能开销,尤其在高频策略回测时。 下一步可把这套账户感知层抽成独立 include,让策略 EA 直接调用而不必关心底层结算模式。外汇与贵金属杠杆交易高风险,账户类型误配可能导致回测与实盘行为偏离。

◍ 对冲与净持账户的持仓逻辑分野

在 MT5 里跟踪账户事件,第一步要分清对冲账户和净持结算账户。两者的根本差别在于同一品种上持仓的表述方式:对冲账户允许同品种同时存在多笔任意方向的仓位,净持账户同一时间只保留一笔单向持仓。 对冲账户里,用逆方向订单去平已有仓位是常规操作。若逆单量小于原仓量,逆单被完全吃掉、原仓仅部分抵消;逆单量更大时,原仓全平、逆单剩残量;两者量相等则双双消失。每笔仓位 ID 等于开仓订单的 ticket,生命周期内不变,且加仓会生成带新 ID 的新仓位。 净持账户没有逆向平仓这回事。逆单触发时,量小于当前仓则部分平,等于则全平,大于则直接反转方向。这里仓位的 ticket 会跟随导致反转的订单,可能和初始 ID 不同;同方向加仓只是把量累加到当前仓,ticket 保持不变。 开 MT5 终端切到「交易」标签,用模拟账户分别开对冲和净持品种各下一单反向单,就能看到持仓列表里一个是新增行、一个是在原行改量或反向,这就是两套模型最直观的差异。外汇和贵金属杠杆高,账户类型选错会直接影响风控脚本的逻辑假设。

净持账户事件跟踪的代码拆分逻辑

净持结算与对冲账户处理仓位的差异,只在仓位事件层明显。把事件处理按账户类型拆进 CEvent 派生类,代码量虽然涨了,但功能隔离后逻辑反而更好排错;等调试稳定再删冗余不迟。 枚举排序踩过坑:事件/订单属性若按类型排序,常量在枚举里的物理位置比编号更关键。不用于搜索的属性要跳过,且未使用的属性一律塞到属性类型列表末尾。初始索引计算时,得用「初始属性索引 − 前面未使用属性数」来修正,否则 ENUM_SORT_ORDERS_MODE 里常量会错位。 验证枚举内容没有自动办法,只能在测试 EA 的 OnInit() 里手敲两个函数调下面的服务函数,把常量名和索引打到流水日志。我当时就这么查出 Defines.mqh 里枚举建错的。 订单分组用抽象订单属性挂一个 Group ID 实现:同 ID 的订单/仓位进同一个列表,默认 0,SetGroupID(group_index) 可改派,GroupID() 可回读。净持事件新增了魔幻数字、逆向品种、持仓反转前后票据等常量,Event.mqh 里 CEvent 私有成员和返回方法都补上了。 净持账户一笔持仓可能多次改向,触发大交易量逆向单时持仓票证换成触发单票证、类型反转,但仓位 ID 不变。要跟踪全生命周期反转,就得捞 ENUM_DEAL_ENTRY 里 DEAL_ENTRY_INOUT 的所有成交,按仓位 ID 拉列表排序,CEventCollection 里 GetDealsByPositionID_InOut() 和 GetPositionByID() 就是干这个的。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在流水日志中显示所有排序枚举常量                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void EnumNumbersTest()
  {
   class="type">class="kw">string enm="ENUM_SORT_ORDERS_MODE";
   class="type">class="kw">string t=StringSubstr(enm,class="num">5,class="num">5)+"BY";
   Print("Search of the values of the enumaration ",enm,":");
   ENUM_SORT_ORDERS_MODE type=class="num">0;
   class="kw">while(StringFind(EnumToString(type),t)==class="num">0)
     {
      Print(enm,"[",type,"]=",EnumToString(type));
      if(type>class="num">500) break;

「库级宏与定时器节奏参数」

在 MT5 函数库的头文件里,用宏把调试信息和运行参数固化下来,能省掉大量重复字符串拼接。下面这段 Defines.mqh 风格的声明,把函数名+行号、界面语言判断、历史数据截止日都压成一条宏。 DFUN_ERR_LINE 会在俄文终端下输出 ', Page ',其他语言输 ', Line ',再拼上 __LINE__ 和冒号。这样报错时直接定位到具体函数与行,不用手动写字符串。 定时器类参数决定了订单/成交集合的轮询频率:TIMER_FREQUENCY 设为 16 毫秒是库定时器最小步长;COLLECTION_PAUSE 为 250 毫秒,是集合刷新之间的硬暂停;COLLECTION_COUNTER_STEP 每跳加 16,配合 ID 0x7778+1~3 区分历史、在场、事件三类集合清单。 开 MT5 把 TIMER_FREQUENCY 改成 32 或 COLLECTION_PAUSE 调到 500,能直观看到脚本占用降低但数据延迟上升,贵金属与外汇品种在高波动时可能漏掉瞬时报价,属高风险调试。

MQL5 / C++
class="macro">#define DFUN_ERR_LINE(__FUNCTION__+(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian" ? ", Page " : ", Line ")+(class="type">class="kw">string)__LINE__+": ")
class="macro">#define DFUN(__FUNCTION__+": ")     class=class="str">"cmt">// "函数描述"
class="macro">#define COUNTRY_LANG("Russian")             class=class="str">"cmt">// 国家语言
class="macro">#define END_TIME(D&class="macro">#x27;class="num">31.12.class="num">3000 class="num">23:class="num">59:class="num">59&class="macro">#x27;) class=class="str">"cmt">// 请求帐户历史记录数据的结束日期
class="macro">#define TIMER_FREQUENCY(class="num">16)                    class=class="str">"cmt">// 函数库定时器的最小频率(以毫秒为单位)
class="macro">#define COLLECTION_PAUSE(class="num">250)                   class=class="str">"cmt">// 订单和成交集合计时器暂停,以毫秒为单位
class="macro">#define COLLECTION_COUNTER_STEP(class="num">16)               class=class="str">"cmt">// 订单和成交集合计时器的计数器增量
class="macro">#define COLLECTION_COUNTER_ID(class="num">1)                 class=class="str">"cmt">// 订单和成交集合计时器的计数器 ID
class="macro">#define COLLECTION_HISTORY_ID(0x7778+class="num">1)          class=class="str">"cmt">// 历史集合清单ID
class="macro">#define COLLECTION_MARKET_ID(0x7778+class="num">2)          class=class="str">"cmt">// 在场集合清单 ID
class="macro">#define COLLECTION_EVENTS_ID(0x7778+class="num">3)          class=class="str">"cmt">// 事件集合列表 ID

◍ 历史订单筛选的枚举定义

在 MT5 的 EA 或脚本里做历史订单统计,第一步往往是先把比较逻辑和时间维度用枚举固化下来,避免后续散落一堆魔法数字。 下面这段代码给出了两组枚举:ENUM_COMPARER_TYPE 管大小关系(等于、多于、少于、不等于、大于等于、小于等于),ENUM_SELECT_BY_TIME 管按哪种时间字段捞单(开仓、平仓,以及带毫秒精度的 OPEN_MSC / CLOSE_MSC)。 实际写回测或复盘工具时,若直接用 SELECT_BY_TIME_CLOSE_MSC 而非 SELECT_BY_TIME_CLOSE,能把同秒多单的排序误差压到 1 毫秒级,对剥头皮策略的成交序列还原更有意义。外汇与贵金属杠杆高,历史统计只是概率参考,不能直接推导下一笔胜负。 把这两个枚举原样拷进你的 include 头文件,后面写 HistorySelectByTime 包装函数就能直接传参,不用每次重写判断分支。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 枚举                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 搜索和排序数据                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_COMPARER_TYPE
  {
   EQUAL,                                                          class=class="str">"cmt">// 等于
   MORE,                                                           class=class="str">"cmt">// 多于
   LESS,                                                           class=class="str">"cmt">// 少于
   NO_EQUAL,                                                       class=class="str">"cmt">// 不等于
   EQUAL_OR_MORE,                                                  class=class="str">"cmt">// 大于等于
   EQUAL_OR_LESS                                                   class=class="str">"cmt">// 小于等于
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按时间排序的可能选项                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_SELECT_BY_TIME
  {
   SELECT_BY_TIME_OPEN,                                            class=class="str">"cmt">// 按开仓时间
   SELECT_BY_TIME_CLOSE,                                           class=class="str">"cmt">// 按平仓时间
   SELECT_BY_TIME_OPEN_MSC,                                        class=class="str">"cmt">// 按开仓时间的毫秒值
   SELECT_BY_TIME_CLOSE_MSC,                                       class=class="str">"cmt">// 按平仓时间的毫秒值
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理订单的数据                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+

把订单状态与整数属性先枚举清楚

在 MT5 里写任何订单扫描逻辑前,先得把「这笔记录到底算什么」说清楚。下面这组枚举把账户里的记录切成 9 类:在场挂单、在场订单、持仓、历史入场订单、已删除挂单、余额操作、信贷操作、成交、以及未知状况。 ORDER_STATUS_MARKET_PENDING 到 ORDER_STATUS_DEAL 覆盖了从挂单到成交的全生命周期,ORDER_STATUS_UNKNOWN 则是接口拿不到明确分类时的兜底。实盘中外汇与贵金属品种在高波动时可能短暂落入 unknown,扫描循环里不能假设永远能对齐前 8 类。 整数属性枚举从 0 号票据开始:ORDER_PROP_TICKET = 0 是每笔记录的唯一 ID,ORDER_PROP_MAGIC 区分 EA 自留单与手动单。时间类属性成对出现——TIME_OPEN 与 TIME_OPEN_MSC 精确到毫秒,TIME_CLOSE 在 MQL5 里对应的是执行或删除时间(ORDER_TIME_DONE),不是你肉眼看到的平仓钟点。 把这两张枚举表直接贴进 include 文件,后面用 OrderGetInteger(ORDER_PROP_TICKET) 取号、用 ORDER_PROP_STATUS 分流,能少写一半判断分支。

MQL5 / C++
enum ENUM_ORDER_STATUS
  {
   ORDER_STATUS_MARKET_PENDING,                                                     class=class="str">"cmt">// 在场挂单
   ORDER_STATUS_MARKET_ORDER,                                                       class=class="str">"cmt">// 在场订单
   ORDER_STATUS_MARKET_POSITION,                                                    class=class="str">"cmt">// 持仓
   ORDER_STATUS_HISTORY_ORDER,                                                      class=class="str">"cmt">// 历史入场订单
   ORDER_STATUS_HISTORY_PENDING,                                                    class=class="str">"cmt">// 已删除挂单
   ORDER_STATUS_BALANCE,                                                            class=class="str">"cmt">// 余额操作
   ORDER_STATUS_CREDIT,                                                             class=class="str">"cmt">// 信贷操作
   ORDER_STATUS_DEAL,                                                               class=class="str">"cmt">// 成交
   ORDER_STATUS_UNKNOWN                                                             class=class="str">"cmt">// 未知状况
  };
enum ENUM_ORDER_PROP_INTEGER
  {
   ORDER_PROP_TICKET = class="num">0,                                                           class=class="str">"cmt">// 订单票据
   ORDER_PROP_MAGIC,                                                                class=class="str">"cmt">// 订单魔幻数字
   ORDER_PROP_TIME_OPEN,                                                            class=class="str">"cmt">// 开单时间 (MQL5 成交时间)
   ORDER_PROP_TIME_CLOSE,                                                           class=class="str">"cmt">// 平单时间 (MQL5 执行或删除时间 - ORDER_TIME_DONE)
   ORDER_PROP_TIME_OPEN_MSC,                                                        class=class="str">"cmt">// 开单时间毫秒值 (MQL5 成交时间毫秒值)
   ORDER_PROP_TIME_CLOSE_MSC,                                                       class=class="str">"cmt">// 平单时间毫秒值 (MQL5 执行或删除时间毫秒值 - ORDER_TIME_DONE_MSC)
   ORDER_PROP_TIME_EXP,                                                             class=class="str">"cmt">// 订单到期日期(针对挂单)
   ORDER_PROP_STATUS,                                                               class=class="str">"cmt">// 订单状况(来自 ENUM_ORDER_STATUS 枚举)
  };

常见问题

净持账户同品种只保留一个双向净额仓位,开反向单会先平部分再开;对冲账户则允许多单并存。切换前先导出持仓快照,避免平错单。
从账户交易事件回调入手,监听持仓净额属性变化,再用定时器轮询确认,别只靠单次推送。
小布可自动监听净持账户仓位与订单状态变动并推送提醒,你只需在品种页开启对应开关即可。
先把订单整数属性与状态全部枚举清楚,再按时间+状态位做位运算筛选,比字符串判断稳。
基础篇建议定时器间隔 200~500 毫秒,宏只做开关与模式区分;过快会增加终端负载,回测时再调低。