用于轻松快速开发 MetaTrader 程序的函数库(第三部分)。 市价订单和仓位的集合,搜索和排序·综合运用
📘

用于轻松快速开发 MetaTrader 程序的函数库(第三部分)。 市价订单和仓位的集合,搜索和排序·综合运用

第 3/3 篇

「正文」

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">const</span> ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); &nbsp;&nbsp; <span class="comment">//--- 构造函数</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CHistoryCollection(); &nbsp;&nbsp; <span class="comment">//--- 更新订单列表,填写新订单数量,并设置交易事件标志</span> &nbsp;&nbsp; <span class="keyword">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Refresh(<span class="keyword">void</span>); &nbsp;&nbsp;}; <span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">public</span>: &nbsp;&nbsp; <span class="comment">//--- 设置 (1) 整数型,(2) 实数型,和 (3) 字符串型订单属性</span> <span style="background-color:rgb(255, 242, 153);">&nbsp;&nbsp; <span class="keyword">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } <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。外汇与贵金属历史数据受点差和跳空影响,这种时间裁剪结果仅反映终端本地历史,不代表真实成交全景,使用需自担高风险。

MQL5 / C++
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 同理记录成交增减,靠这两个整型量能快速判断本轮事件是否有新单子进来。

MQL5 / C++
  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);
};
GetList() 有三个重载版本,分别吃整数、双精度、字符串三种订单属性枚举,再配一个比较模式(默认 EQUAL)。比如想筛出止损价大于某值的订单,就走 double 版 GetList(ORDER_STOP_LOSS, 某值, GREATER)。这种写法比每次手写循环遍历省事,也方便接小布这类 AIGC 工具做批量标注。 测试脚本 TestDoEasyPart03_1.mq5 用枚举 ENUM_TYPE_ORDERS 把市价单、挂单、成交分成三类,输入参数 InpOrderType 默认取 TYPE_ORDER_DEAL。你在 MT5 里把 DoEasy 库放好路径,编译跑一下,改这个输入就能切换看不同类历史数据——外汇和贵金属品种跳空多,成交与挂单差异可能比股票大,属于高风险环境下的正常噪声。

MQL5 / C++
  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 为上月一号,就能在日志里看到那段时间的所有平仓成交。

MQL5 / C++
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_TYPEDEAL_TYPE_BALANCEEQUAL,得到的列表里每条都是余额变动记录。循环里取 COrder* 后调 Print(),终端会逐行吐出明细——跑一次就能核对券商给你的出入金流水是否和本地记录对得上。 外汇和贵金属账户这类操作频繁,余额记录错漏可能影响后续权益计算,手动翻报表效率低,用这段代码 3 秒内就能把全部余额成交打印出来核对。

MQL5 / C++
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 标签里就会看到最盈利与最亏损成交的明细。 外汇与贵金属杠杆高,历史极值仅反映过去,不代表后续倾向;用它做样本分析可以,别当成仓位依据。

MQL5 / C++
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,首启日志干干净净,证明存根生效。外汇与贵金属杠杆高,这类后台通信逻辑先在策略测试器跑稳再挂真户。

MQL5 / C++
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% 的帧率下滑。

MQL5 / C++
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 把下面代码贴进头文件,编译能过就说明你的工程骨架和原文同构。

MQL5 / C++
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 目录结构。

MQL5 / C++
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 的计数器实例做验证。

MQL5 / C++
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 决定编译依赖路径,复制这段代码前先确认你的目录结构与之匹配,否则报找不到文件的错。

MQL5 / C++
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 目录就能编译验证:

MQL5 / C++
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 验证增量是否符合预期。

MQL5 / C++
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 才进后续刷新分支。这套计时器驱动刷新是高波动时段贵金属交易风控脚本的常见写法,但外汇和贵金属杠杆高,任何自动化刷新都不能替代人工盯盘。

MQL5 / C++
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 计时器方法里验证:

MQL5 / C++
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 触发的日志差异即可。外汇与贵金属品种波动剧烈,自动化逻辑须自行压力测试,实盘有较高风险。

MQL5 / C++
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 处理延迟略升,属于可验证的权衡点。外汇与贵金属品种点差跳变频繁,高频定时器下滑点风险偏高,参数调整前建议先开策略测试器跑一轮。

MQL5 / C++
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 未定义。

MQL5 / C++
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 脚本跑一遍,看哪些属性被拦。

MQL5 / C++
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。 外汇与贵金属挂单受点差和滑点影响明显,市价挂单触发后实际成交价可能偏离原挂价,属高风险场景,参数验证请在模拟盘先跑。

MQL5 / C++
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 把这段类骨架补完,跑一遍就能确认事件捕获对不对。

MQL5 / C++
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.mqhMarketPosition.mqh。后两个带 ..\Objects\ 相对路径,意味着挂单与持仓对象被单独抽到了 Objects 目录,编译时若报找不到文件,先核对你的目录层级是否和这套结构一致。 文件头标了 version 1.00 与 2018 年的版权声明,属于早期封装痕迹。做二次开发时别直接改这个头,另起一个继承类去重载方法,能少踩很多链接冲突的坑。

MQL5 / C++
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 间数据确实在变。

MQL5 / C++
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(),就能在日志打出每次总手数变化值。

MQL5 / C++
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。外汇与贵金属杠杆高,账户事件误判可能让你重复下单,建议先在策略测试器用历史成交回放验证计数逻辑。

MQL5 / C++
  };
  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 的字段名对齐自己统计口径,就能直接跑出账户全景快照。外汇与贵金属杠杆高,仓位统计仅作风控参考,不代表方向判断。

MQL5 / C++
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 验证黄金品种手数跳变灵敏度——外汇与贵金属杠杆高,微小仓位变动也可能触发误报,调参前先跑模拟盘。

MQL5 / C++
   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 历史跑一轮确认节奏。

MQL5 / C++
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 之外加节流或计数,否则日志可能被刷屏。

MQL5 / C++
      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 里实测对照。外汇和贵金属杠杆高,跑通逻辑前别急着上实盘。

常见问题

调用按时间窗口检索历史订单的函数,传入起止时间戳即可拉出该区间全部订单,再按需要打印字段。
用筛选余额操作记录的接口单独跑一遍成交历史,能直接把入金出金类记录从交易单里摘出来。
可以,小布能接入你的成交数据按盈亏排序,自动圈出最赚和最亏那两笔并提示对应品种与时间。
多半是没统一按成交时间戳排序,先确保检索接口返回的集合做了时间升序或降序稳定排序再打印。
不建议混搜,市价订单与持仓是不同集合,分开用各自检索接口按时间窗拉取再合并展示更稳。