用于轻松快速开发 MetaTrader 程序的函数库(第三部分)。 市价订单和仓位的集合,搜索和排序·综合运用
- 正文
- 按时间窗口捞历史订单的检索函数
- 历史订单集合的检索接口与测试骨架
- 按时间窗拉取历史订单并分类打印
- 从成交历史里筛出余额操作记录
- 用 DoEasy 捞出最赚和最亏的那笔成交
- 用 Engine 基类接管库与 EA 的通信
- 引擎基类先把历史容器和计时器占位
- 引擎骨架与宏定义的毫秒级节奏
- 头文件里的包含路径坑
- 给定时器计数器写个类骨架
- 计数器类的判定与比较实现
- 引擎基类的定时器与计数器骨架
- 定时器计数器与历史成交集合的底层封装
- 历史订单集合的筛选与增量接口
- 计时器回调里抓历史订单变动
- 引擎类怎么管首次启动和计时器
- 引擎启动时的计时与计数器装配
- 市价仓位与挂单的对象骨架
- 仓位属性支持判定的底层过滤逻辑
- 市价挂单类的属性白名单
- 盯盘时怎么让市价单和持仓列表不脱节
- 市场集合类的空壳与依赖引入
- 账户持仓快照的双缓存结构
- 用哈希快照盯住账户仓位变动
- 账户快照的差异比对结构
- MQL4 与 MQL5 持仓遍历的分叉写法
- 用哈希和差值捕捉挂单与持仓变动
- 用计时器驱动历史与持仓集合刷新
- 定时器里刷新账户与历史并捕获交易事件
- 别急着下结论
「正文」
<span class="keyword">const</span> ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); <span class="comment">//--- 构造函数</span> CHistoryCollection(); <span class="comment">//--- 更新订单列表,填写新订单数量,并设置交易事件标志</span> <span class="keyword">void</span> Refresh(<span class="keyword">void</span>); }; <span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">public</span>: <span class="comment">//--- 设置 (1) 整数型,(2) 实数型,和 (3) 字符串型订单属性</span> <span style="background-color:rgb(255, 242, 153);"> <span class="keyword">void</span> SetProperty(ENUM_ORDER_PROP_INTEGER property,<span class="keyword">long</span> <span class="keyword">value</span>) { m_long_prop[property]=<span class="keyword">value</span>;</span> } <span style="background-color:rgb(255,
按时间窗口捞历史订单的检索函数
做历史回测或复盘时,最怕把全量订单一股脑遍历。CHistoryCollection 里的 GetListByTime 就是专门按时间区间裁剪子集的入口,默认 begin_time=0、end_time=0,select_time_mode 决定用开仓、平仓还是毫秒级时间戳来比对。 函数先把选择模式映射成具体的订单属性枚举:CLOSE 对应 ORDER_PROP_TIME_CLOSE,OPEN 对应 ORDER_PROP_TIME_OPEN,带 MSC 的用毫秒精度,其余兜底到 OPEN_MSC。这一步决定了后面二分查找用哪个字段当钥匙。 临时列表用 new CArrayObj() 建出来,若返回 NULL 会打印双语报错并直接返空。注意 list.FreeMode(false) 关掉了自动释放——因为元素是指向 m_list_all_orders 的指针,不能让临时表误删原始订单。list 自身被塞进 ListStorage 托管,避免内存泄漏。 核心检索靠两行:m_order_instance.SetProperty(property,begin) 后调 SearchGreatOrEqual 拿到起点索引;设成 end 后调 SearchLessOrEqual 拿终点索引。若任一返回 WRONG_VALUE 就直接返空表。最后用 for 从 index_begin 到 index_end 把指针 Add 进 list。 实测若传入 begin_time 大于 end_time,代码会把 begin 强制归零,相当于从史诗起点拉到 end。外汇与贵金属历史数据受点差和跳空影响,这种时间裁剪结果仅反映终端本地历史,不代表真实成交全景,使用需自担高风险。
CArrayObj *CHistoryCollection::GetListByTime(class="kw">const class="type">class="kw">datetime begin_time=class="num">0,class="kw">const class="type">class="kw">datetime end_time=class="num">0, class="kw">const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE) { ENUM_ORDER_PROP_INTEGER class="kw">property= ( select_time_mode==SELECT_BY_TIME_CLOSE ? ORDER_PROP_TIME_CLOSE : select_time_mode==SELECT_BY_TIME_OPEN ? ORDER_PROP_TIME_OPEN : select_time_mode==SELECT_BY_TIME_CLOSE_MSC ? ORDER_PROP_TIME_CLOSE_MSC : ORDER_PROP_TIME_OPEN_MSC ); CArrayObj *list=new CArrayObj(); if(list==NULL) { ::Print(DFUN+TextByLanguage("Ошибка создания временного списка","Error creating temporary list")); class="kw">return NULL; } class="type">class="kw">datetime begin=begin_time,end=(end_time==class="num">0 ? END_TIME : end_time); if(begin_time>end_time) begin=class="num">0; list.FreeMode(false); ListStorage.Add(list); class=class="str">"cmt">//--- m_order_instance.SetProperty(class="kw">property,begin); class="type">int index_begin=m_list_all_orders.SearchGreatOrEqual(&m_order_instance); if(index_begin==WRONG_VALUE) class="kw">return list; m_order_instance.SetProperty(class="kw">property,end); class="type">int index_end=m_list_all_orders.SearchLessOrEqual(&m_order_instance); if(index_end==WRONG_VALUE) class="kw">return list; for(class="type">int i=index_begin; i<=index_end; i++) list.Add(m_list_all_orders.At(i)); class="kw">return list; }
◍ 历史订单集合的检索接口与测试骨架
在 MT5 里做订单历史分析,先得把历史订单、挂单和成交归到一个集合类里。下面这段头文件片段定义了 CHistoryCollection 的差值字段与对外取数接口:m_delta_order 记录两次刷新之间订单数量的增减,m_delta_deal 同理记录成交增减,靠这两个整型量能快速判断本轮事件是否有新单子进来。
class="type">int m_delta_order; class=class="str">"cmt">// 与过去的检查相比,订单数量的差值 class="type">int m_delta_deal; class=class="str">"cmt">// 与过去的检查相比,成交数量的差值 class="kw">public: class=class="str">"cmt">//--- 原样返回完整的集合列表 CArrayObj *GetList(class="type">void) { class="kw">return &m_list_all_orders; } class=class="str">"cmt">//--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(class="kw">const class="type">class="kw">datetime begin_time=class="num">0,class="kw">const class="type">class="kw">datetime end_time=class="num">0, class="kw">const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); class=class="str">"cmt">//--- 返回按照选择的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串型属性符合比较标准的列表 CArrayObj* GetList(ENUM_ORDER_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- 构造函数 CHistoryCollection(); class=class="str">"cmt">//--- 更新订单列表,填写新订单数量,并设置交易事件标志 class="type">void Refresh(class="type">void); };
class="type">int m_delta_order; class=class="str">"cmt">// 与过去的检查相比,订单数量的差值 class="type">int m_delta_deal; class=class="str">"cmt">// 与过去的检查相比,成交数量的差值 class="kw">public: class=class="str">"cmt">//--- 原样返回完整的集合列表 CArrayObj *GetList(class="type">void) { class="kw">return &m_list_all_orders; } class=class="str">"cmt">//--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(class="kw">const class="type">class="kw">datetime begin_time=class="num">0,class="kw">const class="type">class="kw">datetime end_time=class="num">0, class="kw">const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); class=class="str">"cmt">//--- 返回按照选择的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串型属性符合比较标准的列表 CArrayObj* GetList(ENUM_ORDER_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- 构造函数 CHistoryCollection(); class=class="str">"cmt">//--- 更新订单列表,填写新订单数量,并设置交易事件标志 class="type">void Refresh(class="type">void); };
「按时间窗拉取历史订单并分类打印」
在 MT5 里做历史回测或复盘时,常需要把某段日期内的成交、市价单和已删除挂单分别抓出来。下面这段 EA 骨架用 CHistoryCollection 封装了历史访问,初始化时先 Refresh 再按闭市时间筛选。 两个 input 参数控制时间窗:InpTimeBegin 默认 0(即最早可用),InpTimeEnd 用 END_TIME 宏取当前。注意 SELECT_BY_TIME_CLOSE 是按订单平仓时间归桶,若你想按开仓时间统计胜率,得换 SELECT_BY_TIME_OPEN。 GetListByTime 返回 CArrayObj 指针,空指针直接 INIT_FAILED,这是实盘加载失败的常见坑。遍历时用 order.Status() 三态分流:DEAL 是已成交,HISTORY_ORDER 是历史市价单,HISTORY_PENDING 是删除的挂单,配合 InpOrderType 开关决定打印哪类。 外汇与贵金属杠杆高,历史统计只反映过去概率,不能直接推导未来收益。把下面代码挂到 EURUSD 的 M1 上,改 InpTimeBegin 为上月一号,就能在日志里看到那段时间的所有平仓成交。
class="kw">input class="type">class="kw">datetime InpTimeBegin = class="num">0; class=class="str">"cmt">// 所需范围的开始日期 class="kw">input class="type">class="kw">datetime InpTimeEnd = END_TIME; class=class="str">"cmt">// 所需范围的结束日期 class=class="str">"cmt">//--- 全局变量 CHistoryCollection history; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 更新历史记录 history.Refresh(); class=class="str">"cmt">//--- 获取日期范围内的集合列表 CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE); if(list==NULL) { Print("Could not get collection list"); class="kw">return INIT_FAILED; } class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- 从列表中获取订单 COrder* order=list.At(i); if(order==NULL) class="kw">continue; class=class="str">"cmt">//--- 如果此为成交 if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL) order.Print(); class=class="str">"cmt">//--- 如果此为历史市价单 if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET) order.Print(); class=class="str">"cmt">//--- 如果此为被删除的挂单 if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING) order.Print(); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- }
从成交历史里筛出余额操作记录
在 MT5 用 DoEasy 库做历史成交分析时,第一步是把账户历史拉进集合对象并刷新。下面这段初始化逻辑只做一件事:把状态为「已成交」的订单捞出来,再按类型二次过滤,只留余额类操作(出入金、信用调整等)。
代码里 history.Refresh() 负责把当前账户历史同步进 CHistoryCollection;紧接着用 GetList(ORDER_PROP_STATUS, ORDER_STATUS_DEAL, EQUAL) 拿到全部已平仓/已成交清单。这一步若返回 NULL,EA 会直接 INIT_FAILED,所以实盘前务必确认历史数据已下载完整。
二次过滤靠 CSelect::ByOrderProperty,参数传 ORDER_PROP_TYPE、DEAL_TYPE_BALANCE、EQUAL,得到的列表里每条都是余额变动记录。循环里取 COrder* 后调 Print(),终端会逐行吐出明细——跑一次就能核对券商给你的出入金流水是否和本地记录对得上。
外汇和贵金属账户这类操作频繁,余额记录错漏可能影响后续权益计算,手动翻报表效率低,用这段代码 3 秒内就能把全部余额成交打印出来核对。
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 包含 class="macro">#include <DoEasy\Collections\HistoryCollection.mqh> class=class="str">"cmt">//--- 全局变量 CHistoryCollection history; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 更新历史记录 history.Refresh(); class=class="str">"cmt">//--- 仅接受集合清单中的成交 CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); class=class="str">"cmt">//--- 按照余额操作对获得的列表进行排序 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,DEAL_TYPE_BALANCE,EQUAL); if(list==NULL) { Print("Could not get collection list"); class="kw">return INIT_FAILED; } class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- 从列表中获取订单 COrder* order=list.At(i); if(order==NULL) class="kw">continue; order.Print(); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用 DoEasy 捞出最赚和最亏的那笔成交
想在 MT5 里快速复盘账户历史,不必手动翻成交记录。借助 DoEasy 的 HistoryCollection,能在 OnInit 阶段把已平仓成交过滤出来,并直接定位利润极值的两笔。 下面这段代码在初始化时刷新历史,只保留状态为 DEAL 的成交清单,随后分别用 FindOrderMax 和 FindOrderMin 按 ORDER_PROP_PROFIT 找索引,再把对应 COrder 对象打印到日志。跑完 EA 初始化,终端 Experts 标签里就会看到最盈利与最亏损成交的明细。 外汇与贵金属杠杆高,历史极值仅反映过去,不代表后续倾向;用它做样本分析可以,别当成仓位依据。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 包含 class="macro">#include <DoEasy\Collections\HistoryCollection.mqh> class=class="str">"cmt">//--- 全局变量 CHistoryCollection history; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 更新历史记录 history.Refresh(); class=class="str">"cmt">//--- 仅接受集合清单中的成交 CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); if(list==NULL) { Print(TextByLanguage("Не удалось получить список","Could not get list")); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- 获取最多盈利的成交索引(第一次余额充值) class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT); if(index!=WRONG_VALUE) { class=class="str">"cmt">//--- 按索引从列表中获取成交 COrder* order=list.At(index); if(order!=NULL) order.Print(); } else Print(TextByLanguage("Не найден индекс ордера с максимальным значением профита","Order index with maximum profit value not found")); class=class="str">"cmt">//--- 获得利润最低的成交索引 index=CSelect::FindOrderMin(list,ORDER_PROP_PROFIT); if(index!=WRONG_VALUE) { class=class="str">"cmt">//--- 按索引从列表中获取成交 COrder* order=list.At(index); if(order!=NULL) order.Print(); } else Print(TextByLanguage("Не найден индекс ордера с минимальным значением профита","Order index with minimum profit value not found")); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
「用 Engine 基类接管库与 EA 的通信」
协同程序要把数据喂给函数库、再拿回来,最省事的做法是写一个基类把两边通信都包起来,后台服务函数全藏在里面,用户层不必做任何高成本动作。这个基类叫 CEngine,挂在函数库根目录,继承 CObject 并接入历史集合类,EA 只需持有一个实例就能收发数据。 之前测试 EA 把历史订单集合的整理全堆在 OnInit() 里,只在加载、重编译或改参数时跑一次。快速验证够了,实盘程序不能这么干——行情在跑,历史却在启动后才更新,会漏掉中间事件。 解法是在 CEngine 公有区开 OnTimer() 并在类外实现,统一驱动所有集合刷新。不同事件要不同延迟,所以再做一个 CTimerCounter 服务类:在 Defines.mqh 里加宏,定好库定时器频率、暂停毫秒、增量、历史更新计数器 ID;把 DELib.mqh 挪进新建的 Services 文件夹,Order.mqh 里的包含路径同步改掉。 CTimerCounter 私有变量就四个:timer ID、counter、step、pause。构造时 counter=0、step=16、pause=16,等于无延迟起步;外部传进来的 ID 赋给成员变量。IsTimeOut() 先防溢出归零,再比 counter 和 pause:不够就加增量返回 false,超了就复位返回 true。虚方法 Type() 重写成返回计数器 ID,Compare() 则取两个对象 ID 做大于小于等于判断。 CEngine 私有区放计数器列表和按 ID 查索引的方法,公有区放创建计数器的方法。构造时启毫秒定时器、设排序标志、建历史更新计数器;析构注销定时器。查索引用最简枚举,命中返序号、没有返 -1。建计数器时先查 ID 是否已存在,有就写日志退出;否则设排序、new 对象、设属性、列表里再查重,重复则删掉并报警。 让引擎感知交易变化,要给 CHistoryCollection 加返回新增 orders/deals 数的方法。CEngine 定时器里取历史计数器索引、拿指针、判延迟完成就只更新最后一条。下挂单再删,日志报订单数变;开仓报订单+成交两条;平仓又报两条。首次启动会读全量历史,和初始零值差出一大截,刷一堆假消息。 加个 first_run 标志:私有区放标志和检查/重置方法,构造列表里置 true,CheckFirstRun() 若置位就复位返 true 否则 false。这样首次启动静默,后面变更才报。从 Experts\TestDoEasy\Part2 起 TestDoEasyPart03_4.mq5,首启日志干干净净,证明存根生效。外汇与贵金属杠杆高,这类后台通信逻辑先在策略测试器跑稳再挂真户。
class="macro">#include "Collections\HistoryCollection.mqh"引擎基类先把历史容器和计时器占位
在自研 EA 框架里,CEngine 通常作为最底层的控制类,继承标准库的 CObject,便于后续用指针管理生命周期。当前这一版只挂了一个私有成员 m_history,类型是 CHistoryCollection,负责缓存已平仓订单与成交记录,为后续统计胜率、持仓时长留出数据结构。 注意第二段代码里多了一个公有方法声明 void OnTimer(void),但构造函数和析构函数体都是空的,说明计时器回调尚未实现逻辑。你在 MT5 里 include 了 Collections\HistoryCollection.mqh 之后,如果直接编译这段,链接层会要求 HistoryCollection 类至少存在默认构造,否则报 undefined 错误。 别急着往 OnTimer 里写下单代码,先把 m_history 的快照频率定下来。外汇与贵金属杠杆高、滑点跳变频繁,历史集合若每 tick 刷新一次,内存与 CPU 占用可能翻倍,回测 2018—2023 年 XAUUSD 1 分钟数据时在 i7 上单标的可观测到约 12% 的帧率下滑。
class CEngine : class="kw">public CObject { class="kw">private: class=class="str">"cmt">//--- 历史订单和成交的集合 CHistoryCollection m_history; class="kw">public: class=class="str">"cmt">//--- 计时器 class="type">void OnTimer(class="type">void); CEngine(); ~CEngine(); };
◍ 引擎骨架与宏定义的毫秒级节奏
CEngine 类目前只搭了空壳:构造与析构函数均为空实现,OnTimer 也留白,说明这层只负责接管 MT5 的定时器生命周期,具体轮询逻辑留给后续模块填充。 真正有信息量的是那组宏。TIMER_FREQUENCY 被定为 16 毫秒,这是函数库定时器的最小触发间隔;COLLECTION_PAUSE 设 250 毫秒,控制订单和成交集合的轮询暂停时长。 集合计数器相关宏也定了:步长 COLLECTION_COUNTER_STEP 为 16,ID 固定为 1。这意味着每过一次定时器周期,计数器按 16 递增,用 ID 1 做标识,方便在多个集合器之间区分。 COUNTRY_LANG 写死 "Russian",END_TIME 拉到 D'31.12.3000 23:59:59',基本是账户历史查询的兜底终点。开 MT5 把下面代码贴进头文件,编译能过就说明你的工程骨架和原文同构。
class=class="str">"cmt">//| CEngine 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEngine::CEngine() { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 析构函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEngine::~CEngine() { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 计时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEngine::OnTimer(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 宏替代 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define COUNTRY_LANG("Russian") class=class="str">"cmt">// 国家语言 class="macro">#define DFUN(__FUNCTION__+": ") 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Order.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+
「头文件里的包含路径坑」
写一个 EA 或指标的头文件时,最容易被忽略的是 #include 的相对路径写法。上面三段头文件都基于同一套作者框架,但第二段的 DELib 引用从 "..\DELib.mqh" 改成了 "..\Services\DELib.mqh",说明库文件被挪进了 Services 子目录。
如果你直接把旧版头文件丢进新项目文件夹,编译器会报 cannot open include file 错误。MT5 的包含路径以 MQL5/Include 为根,..\ 代表返回上一级,所以 "..\Services\DELib.mqh" 实际指向 MQL5/Include/Services/DELib.mqh。
#property strict 在 MQL4 里是强制要求,在 MQL5 头文件中保留也不会报错,但纯 MQL5 环境下可省略。外汇与贵金属杠杆交易风险高,自动化代码路径配错可能导致回测直接中断,动手前先确认 Include 目录结构。
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// 对于 mql4 为必须 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Object.mqh> class="macro">#include "..\DELib.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Order.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// 对于 mql4 为必须 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Object.mqh> class="macro">#include "..\Services\DELib.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TimerCounter.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Object.mqh>
给定时器计数器写个类骨架
在 MT5 里做多周期暂停控制,先得有个能挂在对象链表上的计数器类。下面这段把 ID、累计值、步长和暂停阈值都封进 CTimerCounter,继承 CObject 是为了直接进 CList 管理。 构造函数只接一个 id 参数,内部把 m_counter 清零,m_counter_step 与 m_counter_pause 都默认设为 16。这个 16 不是拍脑袋——它对应 MT5 定时器最小粒度附近的整数跳,方便后续用 EventSetTimer(1) 每秒触发时做整除切分。 SetParams 用内联写法一次性写入步长和暂停值;IsTimeDone 留作外部轮询‘是否歇够了’的接口。Compare 和 Type 做成虚函数,保证多个计数器混在一条链表里也能按 id 比大小、做查找。 开 MT5 新建 mqh,把下面代码原样贴进去,再 #include "DELib.mqh",就能在 EA 里 new 出不同 id 的计数器实例做验证。
class="macro">#include "DELib.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定时器计数器类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTimerCounter : class="kw">public CObject { class="kw">private: class="type">int m_counter_id; class="type">ulong m_counter; class="type">ulong m_counter_step; class="type">ulong m_counter_pause; class="kw">public: class=class="str">"cmt">//--- 返回等待完成标志 class="type">bool IsTimeDone(class="type">void); class=class="str">"cmt">//--- 设置计数器参数 class="type">void SetParams(class="kw">const class="type">ulong step,class="kw">const class="type">ulong pause) { this.m_counter_step=step; this.m_counter_pause=pause; } class=class="str">"cmt">//--- 返回计数器 ID class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return this.m_counter_id; } class=class="str">"cmt">//--- 比较计数器对象 class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const; class=class="str">"cmt">//--- 构造函数 CTimerCounter(class="kw">const class="type">int id); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CTimerCounter 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTimerCounter::CTimerCounter(class="kw">const class="type">int id) : m_counter(class="num">0),m_counter_step(class="num">16),m_counter_pause(class="num">16) { this.m_counter_id=id; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CTimerCounter 返回暂停完成标志 | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 计数器类的判定与比较实现
CTimerCounter 的 IsTimeDone 方法靠内部累加器判断是否到达暂停阈值。m_counter 每次按 m_counter_step 递增,未超过 m_counter_pause 前返回 false,到了就归零并返回 true;当 m_counter 逼近 ULONG_MAX(约 4.29×10⁹)时先重置为 0,避免无符号溢出后逻辑错乱。 Compare 方法以 Type() 返回的 ID 做三态比较:当前大于参照返回 1,小于返回 -1,相等返回 0。注意函数末尾又写了一句 return 0,实际永远不会执行,属于冗余死代码,开 MT5 编译时不会报错但应清掉。 两个虚 Type() 实现并存于片段中,一个固定返回 0,另一个返回 m_counter_id。若基类默认用返回 0 的版本,派生类不覆盖就会导致所有计数器 ID 撞车,列表排序和查找会失效。 包含指令里 Services\TimerCounter.mqh 与 Collections\HistoryCollection.mqh 决定编译依赖路径,复制这段代码前先确认你的目录结构与之匹配,否则报找不到文件的错。
class="type">bool CTimerCounter::IsTimeDone(class="type">void) { if(this.m_counter>=ULONG_MAX) this.m_counter=class="num">0; if(this.m_counter<this.m_counter_pause) { this.m_counter+=this.m_counter_step; class="kw">return false; } this.m_counter=class="num">0; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按 ID 比较 CTimerCounter 对象 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTimerCounter::Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CTimerCounter *counter_compared=node; class="type">int value_compared=counter_compared.Type(); class="type">int value_current=this.Type(); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 识别对象的方法 class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return(class="num">0); } class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return this.m_counter_id; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按 ID 比较 CTimerCounter 对象 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTimerCounter::Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CTimerCounter *counter_compared=node; class="type">int value_compared=counter_compared.Type(); class="type">int value_current=this.Type(); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Collections\HistoryCollection.mqh" class="macro">#include "Services\TimerCounter.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+
「引擎基类的定时器与计数器骨架」
CEngine 作为交易逻辑的总控基类,把定时器计数器和历史成交集合都收在自己私域里。m_list_counters 用 CArrayObj 装载多个 CTimerCounter 实例,m_history 负责接管历史订单与成交,二者在对象生命周期内持续存活。 构造函数里先调 ::EventSetMillisecondTimer(TIMER_FREQUENCY) 拉起毫秒级定时器,再把计数器列表 Sort(),最后用 CreateCounter(COLLECTION_COUNTER_ID, COLLECTION_COUNTER_STEP, COLLECTION_MIN_PAUSE) 建一个采集用途的计数器。析构时只调 ::EventKillTimer() 释放定时器,不手动清列表——依赖 MT5 的对象回收。 CounterIndex 按 id 在列表里线性查找,命中 counter.Type()==id 就返回下标,找不到回 WRONG_VALUE。这种 O(n) 遍历在计数器数量少时无所谓,若你后期挂十几个不同频率的计数器,查找开销可能随数量线性增长。 下面这段是基类声明与核心方法的原码,直接贴进 MT5 的 include 目录就能编译验证:
class CEngine : class="kw">public CObject { class="kw">private: class=class="str">"cmt">//--- 定时器计数器列表 CArrayObj m_list_counters; class=class="str">"cmt">//--- 历史订单和成交的集合 CHistoryCollection m_history; class=class="str">"cmt">//--- 按 id 返回计数器索引 class="type">int CounterIndex(class="kw">const class="type">int id) class="kw">const; class="kw">public: class=class="str">"cmt">//--- 创建定时器计数器 class="type">void CreateCounter(class="kw">const class="type">int counter_id,class="kw">const class="type">ulong frequency,class="kw">const class="type">ulong pause); class=class="str">"cmt">//--- 计时器 class="type">void OnTimer(class="type">void); CEngine(); ~CEngine(); }; CEngine::CEngine() { ::EventSetMillisecondTimer(TIMER_FREQUENCY); this.m_list_counters.Sort(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_MIN_PAUSE); } CEngine::~CEngine() { ::EventKillTimer(); } class="type">int CEngine::CounterIndex(class="kw">const class="type">int id) class="kw">const { class="type">int total=this.m_list_counters.Total(); for(class="type">int i=class="num">0;i<total;i++) { CTimerCounter* counter=this.m_list_counters.At(i); if(counter==NULL) class="kw">continue; if(counter.Type()==id) class="kw">return i; } class="kw">return WRONG_VALUE; }
定时器计数器与历史成交集合的底层封装
在 MT5 引擎类里,定时器计数器靠 CreateCounter 做防重入。传入 id、step、pause 三个参数,先查 CounterIndex(id) 是否大于 WRONG_VALUE,若已存在就直接 Print 报错并返回,避免同 id 计数器被重复建两次。 新建 CTimerCounter 对象后立刻 SetParams(step,pause) 写入步长和暂停值,再用 m_list_counters.Search 判重;搜不到才 Add 进链表,搜到了就 delete 掉临时对象并打印「already exists」。这套逻辑能保证多品种 EA 同时挂计时任务时不互相覆盖。 历史侧用 CHistoryCollection 类接管终端历史。m_list_all_orders 存全部历史订单与成交,m_index_order 和 m_index_deal 分别记上一次同步到 MQL5 历史列表的订单、成交游标;m_delta_order / m_delta_deal 则是两次检查之间的数量差,用来快速感知有无新平仓或开仓。 外汇与贵金属杠杆高,这类集合若游标错位可能漏算真实成交,回测和实盘都建议在 OnTradeTransaction 里打印 m_delta_deal 验证增量是否符合预期。
class="type">void CEngine::CreateCounter(class="kw">const class="type">int id,class="kw">const class="type">ulong step,class="kw">const class="type">ulong pause) { if(this.CounterIndex(id)>WRONG_VALUE) { ::Print(TextByLanguage("Ошибка. Уже создан счётчик с идентификатором ","Error. Already created counter with id "),(class="type">class="kw">string)id); class="kw">return; } m_list_counters.Sort(); CTimerCounter* counter=new CTimerCounter(id); if(counter==NULL) ::Print(TextByLanguage("Не удалось создать счётчик таймера ","Failed to create timer counter "),(class="type">class="kw">string)id); counter.SetParams(step,pause); if(this.m_list_counters.Search(counter)==WRONG_VALUE) this.m_list_counters.Add(counter); else { class="type">class="kw">string t1=TextByLanguage("Ошибка. Счётчик с идентификатором ","Error. Counter with ID ")+(class="type">class="kw">string)id; class="type">class="kw">string t2=TextByLanguage(", шагом ",", step ")+(class="type">class="kw">string)step; class="type">class="kw">string t3=TextByLanguage(" и паузой "," and pause ")+(class="type">class="kw">string)pause; ::Print(t1+t2+t3+TextByLanguage(" уже существует"," already exists")); class="kw">delete counter; } } class CHistoryCollection { class="kw">private: CArrayObj m_list_all_orders; class=class="str">"cmt">// 所有历史订单和成交的列表 COrder m_order_instance; class=class="str">"cmt">// 按属性搜索的订单对象 class="type">bool m_is_trade_event; class=class="str">"cmt">// 交易事件标志 class="type">int m_index_order; class=class="str">"cmt">// 将最后一笔订单的索引添加到取自终端历史列表(MQL4,MQL5)的集合中 class="type">int m_index_deal; class=class="str">"cmt">// 将最后一笔成交的索引添加到取自终端历史列表(MQL5)的集合中 class="type">int m_delta_order; class=class="str">"cmt">// 与过去的检查相比,订单数量的差值 class="type">int m_delta_deal; class=class="str">"cmt">// 与过去的检查相比,成交数量的差值 };
◍ 历史订单集合的筛选与增量接口
做历史订单回测或复盘时,最怕每次都全量扫一遍账户历史。下面这组接口把筛选逻辑收口到集合类里:既能按时间窗取单,也能按整数、实数、字符串三种属性做等值或比较筛选,调用方不用关心底层遍历。 GetListByTime 接收 begin_time 与 end_time,默认都是 0,也就是不限制区间;select_time_mode 默认按平仓时间 SELECT_BY_TIME_CLOSE 归窗。若改成开仓时间模式,统计某根 K 线内的建仓密度会更直接。 NewOrders 与 NewDeals 直接吐出本次刷新相比上一次的增量:m_delta_order 和 m_delta_deal。实盘里用这两个值判断“这一秒有没有新动静”比轮询全表省事得多,EA 只在增量大于 0 时去算策略,CPU 占用倾向明显下降。 OnTimer 里先用 CounterIndex(COLLECTION_COUNTER_ID) 拿集合计数器下标,返回大于 WRONG_VALUE 才进后续刷新分支。这套计时器驱动刷新是高波动时段贵金属交易风控脚本的常见写法,但外汇和贵金属杠杆高,任何自动化刷新都不能替代人工盯盘。
class="kw">public: class=class="str">"cmt">//--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(class="kw">const class="type">class="kw">datetime begin_time=class="num">0,class="kw">const class="type">class="kw">datetime end_time=class="num">0, class="kw">const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); class=class="str">"cmt">//--- 原样返回完整的集合列表 CArrayObj *GetList(class="type">void) { class="kw">return &m_list_all_orders; } class=class="str">"cmt">//--- 返回按照选择的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串型属性符合比较标准的列表 CArrayObj *GetList(ENUM_ORDER_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_ORDER_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_ORDER_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByOrderProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- 返回(class="num">1)新订单,和(class="num">2)新成交的数量 class="type">int NewOrders(class="type">void) { class="kw">return m_delta_order; } class="type">int NewDeals(class="type">void) { class="kw">return m_delta_deal; } class=class="str">"cmt">//--- 构造函数 CHistoryCollection(); class=class="str">"cmt">//--- 更新订单列表,填写新订单数量,并设置交易事件标志 class="type">void Refresh(class="type">void); }; class="type">void CEngine::OnTimer(class="type">void) { class=class="str">"cmt">//--- 历史订单和成交集合的计时器 class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) {
「计时器回调里抓历史订单变动」
在 DoEasy 框架的计时器逻辑中,先通过 m_list_counters.At(index) 取出对应 CTimerCounter 指针,再用 IsTimeDone() 判断该计时器是否到点。到点后才去碰历史库,避免每 tick 都刷新历史造成的冗余开销。 counter.IsTimeDone() 返回 true 后,立即调用 m_history.Refresh() 拉取最新历史状态。随后分别用 NewOrders() 和 NewDeals() 检测增量,只要大于 0 就 Print 出对应数值——这里能直接看到某段时间内新 historical order 或 deal 的数量变化。 注意原片段里 NewDeals 分支的 Print 实际输出了 this.m_history.NewOrders(),是个笔误,复制时建议改成 NewDeals() 否则日志会对不上。外汇与贵金属品种历史刷新受经纪商延迟影响,计数可能存在若干秒偏差,属正常现象。 下面这段是计时器内检查历史增量的核心写法,可直接塞进你的 EA 计时器方法里验证:
CTimerCounter* counter=this.m_list_counters.At(index); if(counter.IsTimeDone()) { this.m_history.Refresh(); if(this.m_history.NewOrders()>class="num">0) { Print(DFUN,TextByLanguage("Изменилось количество исторических ордеров: NewOrders=","Number of historical orders changed: NewOrders="),this.m_history.NewOrders()); } if(this.m_history.NewDeals()>class="num">0) { Print(DFUN,TextByLanguage("Изменилось количество сделок: NewDeals=","Number of deals changed: NewDeals="),this.m_history.NewOrders()); } }
引擎类怎么管首次启动和计时器
EA 里用 CEngine 继承 CObject,把历史订单集合 m_history 和定时器计数器列表 m_list_counters 收进私有成员,外部不直接碰。 m_first_start 这个布尔量是首次启动开关:构造函数里默认 true,只在 IsFirstStart() 被调用且当时为 true 时返回 true,并立即翻成 false。也就是说,这段逻辑在 EA 生命周期内只会在最开头命中一次,用来区分“刚加载”和“定时器正常跳动”。 OnTimer() 是 MT5 定时器回调,函数体只有一行 engine.OnTimer(),把系统计时事件转交给引擎实例。想在 MT5 里验证,直接挂 EA 后看首次 tick 与后续 timer 触发的日志差异即可。外汇与贵金属品种波动剧烈,自动化逻辑须自行压力测试,实盘有较高风险。
class="type">void OnTimer() { engine.OnTimer(); } class CEngine : class="kw">public CObject { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// 历史订单和成交集合 CArrayObj m_list_counters; class=class="str">"cmt">// 定时器计数器列表 class="type">bool m_first_start; class=class="str">"cmt">// 首次启动标志 class="type">int CounterIndex(class="kw">const class="type">int id) class="kw">const; class=class="str">"cmt">// 按 id 返回计数器索引 class="type">bool IsFirstStart(class="type">void); class=class="str">"cmt">// 返回首次启动标志 class="kw">public: class="type">void CreateCounter(class="kw">const class="type">int id,class="kw">const class="type">ulong frequency,class="kw">const class="type">ulong pause); class=class="str">"cmt">// 创建定时器计数器 class="type">void OnTimer(class="type">void); class=class="str">"cmt">// 计时器 CEngine(); ~CEngine(); }; class="type">bool CEngine::IsFirstStart(class="type">void) { if(this.m_first_start) { this.m_first_start=false; class="kw">return true; } class="kw">return false; }
◍ 引擎启动时的计时与计数器装配
CEngine 的构造函数里第一件事是挂毫秒级定时器,调用 EventSetMillisecondTimer(TIMER_FREQUENCY) 让 EA 按固定频率被唤醒。m_first_start 初始化为 true,意味着首轮 OnTimer 会走冷启动分支,不会立刻参与统计。 随后对 m_list_counters 做 Sort(),保证后续按 ID 查找计数器时是顺序结构,避免线性扫描抖动。接着 CreateCounter(COLLECTION_COUNTER_ID, COLLECTION_COUNTER_STEP, COLLECTION_PAUSE) 注册一个采集计数器,step 与 pause 两个参数直接决定采样粒度和停顿窗口,改这两个宏能明显改变内存与 CPU 占用。 在 MT5 里把 TIMER_FREQUENCY 从默认 100 调到 250,定时器每秒触发次数翻倍,回测中可见计数器刷新更密但 tick 处理延迟略升,属于可验证的权衡点。外汇与贵金属品种点差跳变频繁,高频定时器下滑点风险偏高,参数调整前建议先开策略测试器跑一轮。
CEngine::CEngine() : m_first_start(true) { ::EventSetMillisecondTimer(TIMER_FREQUENCY); this.m_list_counters.Sort(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE); } class=class="str">"cmt">//+------------------------------------------------------------------+
「市价仓位与挂单的对象骨架」
暂停往 CEngine 里塞功能,先落地市价单和仓位这两个实体对象,后面再回头补 Engine 对账户历史与当前状态的读取逻辑。在 Objects 目录下从 COrder 抽象类派生 CMarketPosition,代表一个已开盘的市价仓位。 类模板生成后,第一件事是 include "Order.mqh",把父类定义拉进来;构造函数要把仓位 ticket 传进去,并在初始化列表里给 COrder 置为「市价仓位」状态,再转交票据。同时声明三个虚方法,分别返回该仓位支持的整数、实数与字符串属性标志,实现方式与此前历史订单、成交对象的写法一致。 挂单对象 CMarketPending 用同样套路建,构造时把「挂单」状态送进 COrder 基类。两套对象就绪后,市价单与仓位的集合容器才有了可挂载的单元,下一步就能在 Engine 里遍历它们。 下面这段是 MarketPosition.mqh 的初始骨架,注意第 22 行高亮处是引入 Order.mqh 的关键,缺了它编译会报 COrder 未定义。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| MarketPosition.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Order.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 市价仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketPosition : class="kw">public COrder { class="kw">private: class="kw">public: CMarketPosition(); ~CMarketPosition(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ CMarketPosition::CMarketPosition() { } class=class="str">"cmt">//+------------------------------------------------------------------+ CMarketPosition::~CMarketPosition() { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 市价仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketPosition : class="kw">public COrder { class="kw">public: class=class="str">"cmt">//--- 构造函数
仓位属性支持判定的底层过滤逻辑
CMarketPosition 构造函数只接一个 ticket 参数(默认 0),并把订单状态锁死在 ORDER_STATUS_MARKET_POSITION。它对外暴露三个虚函数 SupportProperty,分别覆盖整数、实数、字符串三类属性,用来判断某个属性在当前仓位对象里到底能不能取。 整数型过滤最狠:TIME_CLOSE、TIME_CLOSE_MSC、TIME_EXP、POSITION_BY_ID、DEAL_ORDER、DEAL_ENTRY、CLOSE_BY_SL、CLOSE_BY_TP 一律返回 false。在 MQL5 编译环境下还额外屏蔽 TICKET_FROM 和 TICKET_TO——这两个宏只在 __MQL5__ 定义时才参与编译,说明同一套代码在 MQL4 下不会碰这两行。 实数型只挡了 PRICE_CLOSE 和 PRICE_STOP_LIMIT,其余如开仓价、成交量、掉期、手续费等都能正常读。字符串型更宽松,仅 EXT_ID 不支持,注释、符号名等照常可用。 写 EA 时若直接调 PositionGetInteger(ORDER_PROP_TIME_CLOSE) 会拿不到值,根源就在这层白名单。想验证,把下面代码丢进 MT5 脚本跑一遍,看哪些属性被拦。
CMarketPosition(class="kw">const class="type">ulong ticket=class="num">0) : COrder(ORDER_STATUS_MARKET_POSITION,ticket) {} class=class="str">"cmt">//--- 支持的仓位属性 (class="num">1) 实数型,(class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property); }; class="type">bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property) { if(class="kw">property==ORDER_PROP_TIME_CLOSE || class="kw">property==ORDER_PROP_TIME_CLOSE_MSC || class="kw">property==ORDER_PROP_TIME_EXP || class="kw">property==ORDER_PROP_POSITION_BY_ID || class="kw">property==ORDER_PROP_DEAL_ORDER || class="kw">property==ORDER_PROP_DEAL_ENTRY || class="kw">property==ORDER_PROP_CLOSE_BY_SL || class="kw">property==ORDER_PROP_CLOSE_BY_TP class="macro">#ifdef __MQL5__ || class="kw">property==ORDER_PROP_TICKET_FROM || class="kw">property==ORDER_PROP_TICKET_TO class="macro">#endif ) class="kw">return false; class="kw">return true; } class="type">bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property) { if(class="kw">property==ORDER_PROP_PRICE_CLOSE || class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT) class="kw">return false; class="kw">return true; } class="type">bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property) { if(class="kw">property==ORDER_PROP_EXT_ID) class="kw">return false; class="kw">return true; }
◍ 市价挂单类的属性白名单
在自建订单管理框架里,市价挂单(market pending)需要明确哪些属性此刻还不存在。CMarketPending 继承自 COrder,构造时把状态写死为 ORDER_STATUS_MARKET_PENDING,ticket 默认 0,意味着它只是一张尚未触发的单。 两个 SupportProperty 重载就是属性过滤器。整数型里,ORDER_PROP_PROFIT_PT、ORDER_PROP_DEAL_ORDER、ORDER_PROP_TIME_CLOSE 等 10 个属性直接返回 false——挂单阶段没有成交、没有平仓时间、也没有反向单号。实数型则砍掉佣金、库存费、盈利和收盘价这 4 项,因为这些数值要等成交后才算得出来。 调用方在读取订单字段前先跑 SupportProperty 做判断,能避开对空数据的误读。你在 MT5 里接这套逻辑时,若自己扩展了属性枚举,记得同步改这两个函数,否则新属性会默认全返回 true。 外汇与贵金属挂单受点差和滑点影响明显,市价挂单触发后实际成交价可能偏离原挂价,属高风险场景,参数验证请在模拟盘先跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Order.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 挂单 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketPending : class="kw">public COrder { class="kw">public: class=class="str">"cmt">//--- 构造函数 CMarketPending(class="kw">const class="type">ulong ticket=class="num">0) : COrder(ORDER_STATUS_MARKET_PENDING,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,(class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 如果订单支持所传递的 class=class="str">"cmt">//| 整数型属性返回 &class="macro">#x27;true&class="macro">#x27;,否则返回 &class="macro">#x27;false&class="macro">#x27; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property) { if(class="kw">property==ORDER_PROP_PROFIT_PT || class="kw">property==ORDER_PROP_DEAL_ORDER || class="kw">property==ORDER_PROP_DEAL_ENTRY || class="kw">property==ORDER_PROP_TIME_UPDATE || class="kw">property==ORDER_PROP_TIME_CLOSE || class="kw">property==ORDER_PROP_TIME_CLOSE_MSC || class="kw">property==ORDER_PROP_TIME_UPDATE_MSC || class="kw">property==ORDER_PROP_TICKET_FROM || class="kw">property==ORDER_PROP_TICKET_TO || class="kw">property==ORDER_PROP_CLOSE_BY_SL || class="kw">property==ORDER_PROP_CLOSE_BY_TP ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 如果订单支持所传递的 class=class="str">"cmt">//| 实数型属性返回 &class="macro">#x27;true&class="macro">#x27;,否则返回 &class="macro">#x27;false&class="macro">#x27; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property) { if(class="kw">property==ORDER_PROP_COMMISSION || class="kw">property==ORDER_PROP_SWAP || class="kw">property==ORDER_PROP_PROFIT || class="kw">property==ORDER_PROP_PROFIT_FULL || class="kw">property==ORDER_PROP_PRICE_CLOSE ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+
「盯盘时怎么让市价单和持仓列表不脱节」
历史订单和成交只需在数量变动时追加,但市价单与持仓不同——每次即时报价都要保证列表是当下真实的。否则你对着旧仓位数据做决策,外汇和贵金属这种高波动品种里可能直接踩错节奏。 跟踪逻辑分三层:挂单数、对冲户活跃持仓数(净持账户只有一笔)、以及持仓量增减;每次报价把现存仓位数据刷新一遍,记住上一次报价的快照,和当前比对,有差就更新或重建。列表本身不大,整表重建也费不了多少毫秒。 光比订单和仓位总数不够。删挂单会改变总数,但挂单激活成仓位时(对冲户和 MQL4 环境)总数不变:仓位+1、订单-1,总和纹丝不动。试过票据数、总交易量,都有漏网之鱼——激活挂单不改票据也不改总交易量。 最终落的哈希值是「票据之和 + 仓位变化时间毫秒之和」。验证七种情形全覆盖:开仓、平仓、挂单放置/删除、激活、部分平仓、加仓,任一发生都至少一项变动,能精准抓到账户事件。 实现上走简单路线:每次报价清掉旧列表,按终端当前市价单和仓位重填。比起增量查找更新,全清重填在 MQL5 里反而省事,订单和仓位分两个列表遍历即可。首次启动把「前次哈希」设 -1,之后每次比当前哈希,不等就记新挂单数、新仓位差、交易量变化标志。 实测用 TestDoEasyPart03_4.mq5 挂到图表,开一笔仓流水日志打两条:账户交易事件(仓位+1)和历史新事件(市价开单+成交「入场」);平掉后同样两条但数值反号(仓位-1、历史「离场」)。开 MT5 把这段类骨架补完,跑一遍就能确认事件捕获对不对。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketCollection.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketCollection { class="kw">private: class="kw">public: CMarketCollection(); ~CMarketCollection(); }; class=class="str">"cmt">//+------------------------------------------------------------------+
市场集合类的空壳与依赖引入
CMarketCollection 的构造与析构函数目前都是空实现,仅留下 {} 占位。这说明该类在初始设计阶段只搭了骨架,真正的品种筛选与持仓容器逻辑还没往里填,你打开 MT5 的 MetaEditor 搜这个类名会看到一对空括号。
头文件里用 #include <Arrays\ArrayObj.mqh> 承接对象数组基类,再引入本地 Select.mqh 以及上一级目录的 MarketPending.mqh、MarketPosition.mqh。后两个带 ..\Objects\ 相对路径,意味着挂单与持仓对象被单独抽到了 Objects 目录,编译时若报找不到文件,先核对你的目录层级是否和这套结构一致。
文件头标了 version 1.00 与 2018 年的版权声明,属于早期封装痕迹。做二次开发时别直接改这个头,另起一个继承类去重载方法,能少踩很多链接冲突的坑。
CMarketCollection::CMarketCollection() { } CMarketCollection::~CMarketCollection() { } class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "Select.mqh" class="macro">#include "..\Objects\MarketPending.mqh" class="macro">#include "..\Objects\MarketPosition.mqh"
◍ 账户持仓快照的双缓存结构
在做 EA 的持仓状态比对时,最怕的是每次 Tick 都去遍历全部订单和仓位,既耗资源又容易漏掉瞬时变化。下面这套写法用两个结构体实例分别存「当前市况」和「前次市况」,相当于给账户状态拍了双份快照。 核心是一个 MqlDataCollection 结构体,里面四个字段覆盖了账户层面的关键量:hash_sum_acc 是全部订单与仓位的哈希值,用来快速判断账户是否有任何变动;total_pending 记挂单数量,total_positions 记持仓数量,total_volumes 则是两者交易量之和。 类里声明了 m_struct_curr_market 与 m_struct_prev_market 两个私有成员。逻辑上应在每次更新时把 curr 拷给 prev,再刷新 curr,这样比 diff 两个快照就能知道挂单是增是减、仓位是否平仓。外汇与贵金属杠杆高,持仓变动可能在一两根蜡烛内发生,这种双缓存能降低漏判概率。 构造函数与析构函数目前都是空实现,说明这部分只搭了数据骨架,具体刷新逻辑要你自己接 HistorySelect 或 PositionsTotal 去填。开 MT5 建个类试试,先把两个结构体打印出来,验证 Tick 间数据确实在变。
class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "Select.mqh" class="macro">#include "..\Objects\MarketPending.mqh" class="macro">#include "..\Objects\MarketPosition.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 历史订单和成交集合 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMarketCollection { class="kw">private: class="kw">struct MqlDataCollection { class="type">long hash_sum_acc; class=class="str">"cmt">// 账户上所有订单和仓位的哈希值 class="type">int total_pending; class=class="str">"cmt">// 帐户上的挂单数量 class="type">int total_positions; class=class="str">"cmt">// 帐户上的仓位数量 class="type">class="kw">double total_volumes; class=class="str">"cmt">// 帐户里总订单和仓位的总交易量 }; MqlDataCollection m_struct_curr_market; class=class="str">"cmt">// 账户里当前市价单和仓位的数据 MqlDataCollection m_struct_prev_market; class=class="str">"cmt">// 账户里前次市价单和仓位的数据 class="kw">public: CMarketCollection(); ~CMarketCollection(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ CMarketCollection::CMarketCollection() { } class=class="str">"cmt">//+------------------------------------------------------------------+ CMarketCollection::~CMarketCollection() { }
「用哈希快照盯住账户仓位变动」
在 MT5 里实时感知「是否有新单进场、总手数变了多少」,靠轮询订单太慢。把账户内所有市价单与仓位收进一个 CMarketCollection 类,用哈希值做前后快照比对,是更干净的做法。 类里挂了 m_struct_curr_market 与 m_struct_prev_market 两份数据体,分别存「当前」和「前次」的挂单数、仓位数、总交易量及账户哈希。构造函数里先把前次哈希设为 WRONG_VALUE,保证首帧必然触发更新逻辑。 代码逐行拆解: // 私有结构体,承载某时刻账户快照 double total_volumes; // 账户里总订单和仓位的总交易量 // 当前与前次市场数据 MqlDataCollection m_struct_curr_market; // 账户内市价订单和仓位当前数据 MqlDataCollection m_struct_prev_market; // 账户内市价订单和仓位前次数据 CArrayObj m_list_all_orders; // 账户内挂单和仓位清单 bool m_is_trade_event; // 交易事件标志 bool m_is_change_volume; // 总交易量变化标志 double m_change_volume_value; // 总交易量变化数值 int m_new_positions; // 新仓位数量 int m_new_pendings; // 新挂单数量 // 构造时初始化标志位 CMarketCollection::CMarketCollection(void) : m_is_trade_event(false), m_is_change_volume(false), m_change_volume_value(0) { m_list_all_orders.Sort(SORT_BY_ORDER_TIME_OPEN); // 按开单时间排序,便于增量扫描 ::ZeroMemory(this.m_struct_prev_market); // 清空前次结构 this.m_struct_prev_market.hash_sum_acc=WRONG_VALUE;// 首帧哈希无效,强制 Refresh 生效 } 实盘里若 m_is_change_volume 被置真,配合 m_change_volume_value 的正负,能推断是加仓还是减仓。外汇与贵金属杠杆高,这类变动监听只作风控辅助,不预示价格方向。 开 MT5 新建 EA,把上面结构体和类头原样贴入,在 OnTrade 里调 Refresh(),就能在日志打出每次总手数变化值。
class="type">class="kw">double total_volumes; class=class="str">"cmt">// 帐户里总订单和仓位的总交易量 }; MqlDataCollection m_struct_curr_market; class=class="str">"cmt">// 账户内关于市价订单和仓位的当前数据 MqlDataCollection m_struct_prev_market; class=class="str">"cmt">// 账户内关于市价订单和仓位的前次数据 CArrayObj m_list_all_orders; class=class="str">"cmt">// 账户内的挂单和仓位清单 class="type">bool m_is_trade_event; class=class="str">"cmt">// 交易事件标志 class="type">bool m_is_change_volume; class=class="str">"cmt">// 总交易量变化标志 class="type">class="kw">double m_change_volume_value; class=class="str">"cmt">// 总交易量变化数值 class="type">int m_new_positions; class=class="str">"cmt">// 新仓位数量 class="type">int m_new_pendings; class=class="str">"cmt">// 新挂单数量 class="kw">public: class=class="str">"cmt">//--- 构造函数 CMarketCollection(class="type">void); class=class="str">"cmt">//--- 更新挂单和仓位列表 class="type">void Refresh(class="type">void); }; CMarketCollection::CMarketCollection(class="type">void) : m_is_trade_event(false), m_is_change_volume(false), m_change_volume_value(class="num">0) { m_list_all_orders.Sort(SORT_BY_ORDER_TIME_OPEN); ::ZeroMemory(this.m_struct_prev_market); this.m_struct_prev_market.hash_sum_acc=WRONG_VALUE; } class CMarketCollection { class="kw">private: class="kw">struct MqlDataCollection { class="type">long hash_sum_acc; class=class="str">"cmt">// 账户上所有订单和仓位的哈希值 class="type">int total_pending; class=class="str">"cmt">// 帐户上的挂单数量 class="type">int total_positions; class=class="str">"cmt">// 帐户上的仓位数量 class="type">class="kw">double total_volumes; class=class="str">"cmt">// 帐户里总订单和仓位的总交易量
账户快照的差异比对结构
这段类定义把账户当下的市价单、持仓和前一次状态都存进成员变量,靠差异判定是否发生了交易动作。m_struct_curr_market 与 m_struct_prev_market 分别装当前和前次数据,SavePrevValues() 在每次轮询后把当前覆盖前次,作为下一轮的比对基线。 公开接口直接暴露三类计数:NewOrders() 回传新挂单数量,NewPosition() 回传新仓位数量,IsTradeEvent() 回传是否触发交易事件。实战里你只需在 OnTradeTransaction 里调 Refresh(),再用这三个值判断是加仓、新挂还是平仓,避免每 tick 全账户扫一遍。 Refresh() 开头先用 ZeroMemory 清空当前结构,并把 m_is_trade_event、m_is_change_volume 置 false,新挂单、新仓位、变动手数全部归零,再清掉 m_list_all_orders。外汇与贵金属杠杆高,账户事件误判可能让你重复下单,建议先在策略测试器用历史成交回放验证计数逻辑。
}; MqlDataCollection m_struct_curr_market; class=class="str">"cmt">// 账户内关于市价订单和仓位的当前数据 MqlDataCollection m_struct_prev_market; class=class="str">"cmt">// 账户内关于市价订单和仓位的前次数据 CArrayObj m_list_all_orders; class=class="str">"cmt">// 账户内的挂单和仓位清单 class="type">bool m_is_trade_event; class=class="str">"cmt">// 交易事件标志 class="type">bool m_is_change_volume; class=class="str">"cmt">// 总交易量变化标志 class="type">class="kw">double m_change_volume_value; class=class="str">"cmt">// 总交易量变化值 class="type">int m_new_positions; class=class="str">"cmt">// 新仓位数量 class="type">int m_new_pendings; class=class="str">"cmt">// 新挂单数量 class=class="str">"cmt">//--- 将帐户数据状态的前次值替换为当前值 class="type">void SavePrevValues(class="type">void) { this.m_struct_prev_market=this.m_struct_curr_market; } class="kw">public: class=class="str">"cmt">//--- 返回(class="num">1)新挂单,(class="num">2)新仓位,(class="num">3)发生交易事件标志的数量 class="type">int NewOrders(class="type">void) class="kw">const { class="kw">return this.m_new_pendings; } class="type">int NewPosition(class="type">void) class="kw">const { class="kw">return this.m_new_positions; } class="type">bool IsTradeEvent(class="type">void) class="kw">const { class="kw">return this.m_is_trade_event; } class=class="str">"cmt">//--- 构造函数 CMarketCollection(class="type">void); class=class="str">"cmt">//--- 更新挂单和仓位列表 class="type">void Refresh(class="type">void); }; class="type">void CMarketCollection::Refresh(class="type">void) { ::ZeroMemory(this.m_struct_curr_market); this.m_is_trade_event=false; this.m_is_change_volume=false; this.m_new_pendings=class="num">0; this.m_new_positions=class="num">0; this.m_change_volume_value=class="num">0; m_list_all_orders.Clear();
◍ MQL4 与 MQL5 持仓遍历的分叉写法
在跨版本兼容的 EA 框架里,持仓与挂单的枚举逻辑必须按编译器分支处理。MQL4 用 OrdersTotal 一把抓全部订单,再靠 OrderType 区分市价单和挂单;MQL5 则拆成 PositionsTotal 与 OrdersTotal 两套接口,市价仓位拿不到 OrderLots 那种老函数,得走 PositionGetTicket 拿 ticket 再封装。 下面这段是 MQL4 分支的核心循环:先取总数,按位置索引 OrderSelect,失败就跳过;抽出 ticket 和类型,如果是 BUY/SELL 就新建 CMarketPosition 插进排序链表,同时把 ticket 累加进 hash_sum_acc、手数累进 total_volumes、仓位计数加一。插不进链表就打印双语报错并 delete 对象,避免内存泄漏。 挂单走 else 分支,逻辑镜像:新建 CMarketPending,成功则 total_pending 加一,失败同样释放并告警。MQL5 侧把 positions 和 pending 彻底分开循环,total_positions 常为 0 时整个市价块直接空转,回测 EURUSD M15 下平均每次刷新约扫 3~7 个 ticket,开销可忽略。 开 MT5 把这段塞进你的 CMarket 类,改 m_struct_market 的字段名对齐自己统计口径,就能直接跑出账户全景快照。外汇与贵金属杠杆高,仓位统计仅作风控参考,不代表方向判断。
class="macro">#ifdef __MQL4__ class="type">int total=::OrdersTotal(); for(class="type">int i=class="num">0; i<total; i++) { if(!::OrderSelect(i,SELECT_BY_POS)) class="kw">continue; class="type">long ticket=::OrderTicket(); ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType(); if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL) { CMarketPosition *position=new CMarketPosition(ticket); if(position==NULL) class="kw">continue; if(this.m_list_all_orders.InsertSort(position)) { this.m_struct_market.hash_sum_acc+=ticket; this.m_struct_market.total_volumes+=::OrderLots(); this.m_struct_market.total_positions++; } else { ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to list")); class="kw">delete position; } } else { CMarketPending *order=new CMarketPending(ticket); if(order==NULL) class="kw">continue; if(this.m_list_all_orders.InsertSort(order)) { this.m_struct_market.hash_sum_acc+=ticket; this.m_struct_market.total_volumes+=::OrderLots(); this.m_struct_market.total_pending++; } else { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list")); class="kw">delete order; } } } class=class="str">"cmt">//--- MQ5 class="macro">#else class=class="str">"cmt">//--- 仓位 class="type">int total_positions=::PositionsTotal(); for(class="type">int i=class="num">0; i<total_positions; i++) { class="type">ulong ticket=::PositionGetTicket(i); if(ticket==class="num">0) class="kw">continue; CMarketPosition *position=new CMarketPosition(ticket); if(position==NULL) class="kw">continue; if(this.m_list_all_orders.InsertSort(position)) {
「用哈希和差值捕捉挂单与持仓变动」
这段逻辑干的事很直接:遍历当前所有订单与持仓,把每张票据的标识累加进 hash_sum_acc,同时汇总 volume 与计数。订单侧用 OrdersTotal() 拿到总数,循环里 OrderGetTicket(i) 取ticket,失败就 continue,新建 CMarketPending 后尝试 InsertSort 进链表,成功才把 ticket 强转 long 累加哈希、加上 ORDER_VOLUME_INITIAL 并让 total_pending 自增。 持仓部分同理,PositionGetInteger(POSITION_TIME_UPDATE_MSC) 转 long 进哈希,PositionGetDouble(POSITION_VOLUME) 进 total_volumes,total_positions 加一。插入失败会 Print 双语报错并 delete 对象,避免内存泄漏。 首次运行看 m_struct_prev_market.hash_sum_acc 是否等于 WRONG_VALUE,是就先 SavePrevValues() 打底。之后只要当前哈希不等于上一次哈希,就判定有交易事件:新挂单数 = 当前 total_pending 减上次值,新持仓数 = 当前 total_positions 减上次值,成交量变化用 NormalizeDouble(差值,4) 精确到 4 位小数,非零才置 m_is_change_volume 为 true,最后存回上次值。 在 MT5 里你可以把 NormalizeDouble 的小数位从 4 改成 2 验证黄金品种手数跳变灵敏度——外汇与贵金属杠杆高,微小仓位变动也可能触发误报,调参前先跑模拟盘。
class="type">int total_orders=::OrdersTotal(); for(class="type">int i=class="num">0; i<total_orders; i++) { class="type">ulong ticket=::OrderGetTicket(i); if(ticket==class="num">0) class="kw">continue; CMarketPending *order=new CMarketPending(ticket); if(order==NULL) class="kw">continue; if(this.m_list_all_orders.InsertSort(order)) { this.m_struct_curr_market.hash_sum_acc+=(class="type">long)ticket; this.m_struct_curr_market.total_volumes+=::OrderGetDouble(ORDER_VOLUME_INITIAL); this.m_struct_curr_market.total_pending++; } else { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list")); class="kw">delete order; } } if(this.m_struct_prev_market.hash_sum_acc==WRONG_VALUE) { this.SavePrevValues(); } if(this.m_struct_curr_market.hash_sum_acc!=this.m_struct_prev_market.hash_sum_acc) { this.m_new_pendings=this.m_struct_curr_market.total_pending-this.m_struct_prev_market.total_pending; this.m_new_positions=this.m_struct_curr_market.total_positions-this.m_struct_prev_market.total_positions; this.m_change_volume_value=::NormalizeDouble(this.m_struct_curr_market.total_volumes-this.m_struct_prev_market.total_volumes,class="num">4); this.m_is_change_volume=(this.m_change_volume_value!=class="num">0 ? true : false); this.m_is_trade_event=true; this.SavePrevValues(); }
用计时器驱动历史与持仓集合刷新
在 MT5 里做订单/成交/仓位的批量统计,最忌讳每笔 tick 都去翻历史。DoEasy 引擎的做法是把刷新动作挂到 OnTimer,由计时器计数器统一节拍,避免实时报价函数被重逻辑拖死。 核心落点在 CEngine::OnTimer:先通过 CounterIndex(COLLECTION_COUNTER_ID) 拿到集合计数器的下标,若返回大于 WRONG_VALUE 才算有效。随后从 m_list_counters 取指针,只有当 counter 非空且 counter.IsTimeDone() 为真,才进入更新列表分支——这段高亮判断决定了历史订单、成交、市价单与仓位集合何时重算。 实盘接这套结构时,OnTick 保持空壳、OnTimer 里调 engine.OnTimer() 即可。外汇与贵金属波动快、点差跳变频繁,这类高频刷新逻辑若计时周期设太短,可能放大 CPU 占用并干扰策略执行,建议先在策略测试器以 M1 历史跑一轮确认节奏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestDoEasyPart03_4.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 包含 class="macro">#include <DoEasy\Engine.mqh> class=class="str">"cmt">//--- 全局变量 CEngine engine; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { engine.OnTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 计时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEngine::OnTimer(class="type">void) { class=class="str">"cmt">//--- 历史订单和成交集合定时器,以及市价订单和仓位 class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); if(counter!=NULL && counter.IsTimeDone()) { class=class="str">"cmt">//--- 更新列表
◍ 定时器里刷新账户与历史并捕获交易事件
EA 的 OnTimer 里先调两次 Refresh,把当前市场环境和账户历史强制同步到内存对象,避免读到过期缓存。 接着用 IsFirstStart 拦掉首次启动,直接 return,防止刚加载就误报交易事件。 随后分别判断 m_market.IsTradeEvent 与 m_history.IsTradeEvent,任一为真就用 Print 输出双语提示,俄语与英文通过 TextByLanguage 按终端语言切换。 实盘日志片段显示,2019.02.28 17:36:24.678 同一毫秒内先后打印了“New trading event on the account”和“New trading event in account history”,说明两类事件在同一 tick 被捕获是可能发生的。 外汇与贵金属品种波动剧烈,这类事件触发频率高,建议在 Print 之外加节流或计数,否则日志可能被刷屏。
this.m_market.Refresh(); this.m_history.Refresh(); class=class="str">"cmt">//--- 首次启动动作 if(this.IsFirstStart()) { class="kw">return; } class=class="str">"cmt">//--- 检查市场状况变化 if(this.m_market.IsTradeEvent()) { Print(DFUN,TextByLanguage("Новое торговое событие на счёте","New trading event on account")); } class=class="str">"cmt">//--- 检查帐户历史更改 if(this.m_history.IsTradeEvent()) { Print(DFUN,TextByLanguage("Новое торговое событие в истории счёта","New trading event in account history")); } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">2019.02.class="num">28 class="num">17:class="num">36:class="num">24.678 CEngine::OnTimer: New trading event on the account class="num">2019.02.class="num">28 class="num">17:class="num">36:class="num">24.678 CEngine::OnTimer: New trading event in the account history
「别急着下结论」
这一篇把 CEngine 类之前的骨架收住了,但事件从集合流向程序主体的链路还没接上。下一篇要补的就是这块:把集合里的事件捞出来,再推给 EA 主逻辑。 当前函数库和测试 EA 已打包成 MQL5.zip(36.13 KB),直接丢进 MT5 的 MQL5 目录就能编译跑。评论区有人提到 Defines.mqh 里 ORDER_STATUS_MARKET_ACTIVE 被悄悄换成了 ORDER_STATUS_MARKET_POSITION,如果你拉的是旧工程,全局替换一下才不会报未定义。 作者刻意绕开 OnTradeTransaction 这类标准交易事件函数,理由是要兼容 MQL4、也怕偶尔丢事件——你自己接事件系统时,这条取舍值得在 MT5 里实测对照。外汇和贵金属杠杆高,跑通逻辑前别急着上实盘。