用于轻松快速开发 MetaTrader 程序的函数库(第三部分)。 市价订单和仓位的集合,搜索和排序·进阶篇
(2/3)· 当订单仓位堆成列表,手动遍历既慢又易漏,这篇把集合检索和排序一次讲透
接上篇,我们继续深挖函数库的核心层。许多开发者在拿到市价订单和仓位列表后,仍用循环硬筛,既难维护又容易在并发更新时漏掉对象。本篇把 Engine 基准对象和 CSelect 搜索类落地,让集合操作变成一行调用。
◍ 按属性筛订单的三套重载逻辑
在订单筛选类里,按属性取值做条件过滤被拆成了三套重载函数,分别对应整数型、实数型和字符串型订单属性。调用方只需传入源订单数组、属性枚举、目标值和比较模式,就能拿到一张新的订单指针列表。 以字符串属性版本为例,函数先判空 list_source,为空直接返 NULL;随后 new 一个 CArrayObj 作为结果容器,并将其 FreeMode 设为 false,避免自动释放外部传入的订单对象。结果列表还会被塞进 ListStorage 统一管理内存。 循环里对源数组逐个取 COrder 指针,若订单不支持该字符串属性则 continue 跳过;支持则用 GetProperty 取实际值,交给 CompareValues 按 mode 比较,命中条件才 Add 进结果列表。实数型与整数型版本结构完全一致,仅把 string/double/long 换了类型。 这套写法的实用点在于:你可以在 EA 里用 ByOrderProperty 直接捞出黄金订单,再接别的筛选。外汇与贵金属杠杆高、滑点跳空频繁,实盘使用前建议在 MT5 策略测试器用历史 tick 验证过滤结果是否符合预期。
class="type">long order_prop=order.GetProperty(class="kw">property); if(CompareValues(order_prop,value,mode)) list.Add(order); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回订单列表的实数型属性 | class=class="str">"cmt">//| 订单需要符合指定条件 | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); ListStorage.Add(list); for(class="type">int i=class="num">0; i<list_source.Total(); i++) { COrder *order=list_source.At(i); if(!order.SupportProperty(class="kw">property)) class="kw">continue; class="type">class="kw">double order_prop=order.GetProperty(class="kw">property); if(CompareValues(order_prop,value,mode)) list.Add(order); } class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回订单列表的字符串型属性 | class=class="str">"cmt">//| 订单需要符合指定条件 | class=class="str">"cmt">//+------------------------------------------------------------------+ CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); ListStorage.Add(list); for(class="type">int i=class="num">0; i<list_source.Total(); i++) { COrder *order=list_source.At(i); if(!order.SupportProperty(class="kw">property)) class="kw">continue; class="type">class="kw">string order_prop=order.GetProperty(class="kw">property); if(CompareValues(order_prop,value,mode)) list.Add(order); } class="kw">return list; }
「订单极值检索的静态接口与实现」
CSelect 类把订单筛选做成了一批静态方法,调用方不需要实例化,直接丢一个 CArrayObj 源列表进去就能拿结果。按属性类型分了三套重载:整数(ENUM_ORDER_PROP_INTEGER)、实数(ENUM_ORDER_PROP_DOUBLE)、字符串(ENUM_ORDER_PROP_STRING),分别对应 ByOrderProperty 过滤和 FindOrderMax / FindOrderMin 取极值索引。 FindOrderMax 的整数版本实现很直白:先判空和总数,total 为 0 直接返回 WRONG_VALUE;然后从 i=1 开始遍历,把当前订单属性和 index 位置已记录的最大订单属性丢给 CompareValues,比较模式传 MORE,成立就更新 index。最终返回的 index 是源列表里该整数属性最大的那个订单下标,不是订单 ticket。 实盘里这类接口适合用来从一坨挂单里挑出止损距离最大或手数最小的那个,但外汇和贵金属杠杆高、滑点随机,极值订单不代表最优执行,回测和实盘可能偏离。 下面这段是 FindOrderMax 整数属性的核心代码,逐行看逻辑落点: int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property) { // 函数入口,接收源列表和要比较的整数属性枚举 if(list_ return WRONG_VALUE; // 源列表空指针,直接错值返回 int index=0; // 默认最大者下标为 0 COrder *max_order=NULL; // 暂存最大订单指针,实际循环里被覆盖 int total=list_source.Total(); // 取列表总数 if(total==0) return WRONG_VALUE; // 空列表直接返回错值 for(int i=1; i<total; i++) // 从第二个元素开始比 { COrder *order=list_source.At(i); // 取当前遍历订单 long order1_prop=order.GetProperty(property); // 当前订单属性值 max_order=list_source.At(index); // 取已记录最大订单 long order2_prop=max_order.GetProperty(property); // 已记录最大属性值 if(CompareValues(order1_prop,order2_prop,MORE)) index=i; // 大于则更新下标 } return index; // 返回最大属性对应的列表下标 }
class="type">int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; COrder *max_order=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { COrder *order=list_source.At(i); class="type">long order1_prop=order.GetProperty(class="kw">property); max_order=list_source.At(index); class="type">long order2_prop=max_order.GetProperty(class="kw">property); if(CompareValues(order1_prop,order2_prop,MORE)) index=i; } class="kw">return index; }
按属性极值挑订单索引的实现细节
在订单筛选类里,按某一属性找最大或最小订单,核心思路是先锁死索引 0 为基准,再从 i=1 开始向后比。列表为空直接吐 WRONG_VALUE,避免后续空指针。 下面这段是按双精度属性找最大值的循环骨架,注意每次都比较当前元素与基准索引处元素,只有「更大」才把 index 推到 i: int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { COrder *order=list_source.At(i); double order1_prop=order.GetProperty(property); max_order=list_source.At(index); double order2_prop=max_order.GetProperty(property); if(CompareValues(order1_prop,order2_prop,MORE)) index=i; } return index; 字符串属性最大值版本逻辑一致,只是 GetProperty 返回 string,CompareValues 的比较方向仍为 MORE。整数属性最小值则把属性类型换成 long,比较宏换成 LESS,从 i=1 起找更小者。 开 MT5 把这三段塞进你的 CSelect 类,挂上 EURUSD 的十五分钟图跑一遍,列表长度在 0–200 笔时索引返回应当稳定;外汇与贵金属杠杆高,回测结果仅作逻辑验证,实盘仍可能因点差跳变而偏离。
class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { COrder *order=list_source.At(i); class="type">class="kw">double order1_prop=order.GetProperty(class="kw">property); max_order=list_source.At(index); class="type">class="kw">double order2_prop=max_order.GetProperty(class="kw">property); if(CompareValues(order1_prop,order2_prop,MORE)) index=i; } class="kw">return index;
◍ 在订单列表里挑出属性最小的那一笔
做批量订单筛选时,经常要从一个 CArrayObj 订单容器里找出某个属性取值最小的那一单。下面这组重载函数用实数属性和字符串属性分别实现了这个需求,调用方只需传容器指针和属性枚举。 两个函数都先取 total=list_source.Total(),若 total==0 直接返回 WRONG_VALUE,避免空列表引发越界。遍历从 i=1 开始,始终拿当前下标 index 对应的订单做基准,用 CompareValues(...,LESS) 判断是否需要更新 index。 实数版取 double 属性做比较,字符串版取 string 属性做字典序比较,逻辑完全对称。实际跑起来,若列表有 50 笔持仓,函数会在一次 O(n) 扫描内返回最小属性订单的下标,时间开销可忽略。 把这段直接塞进你的 CSelect 类,就能在 EA 里快速定位例如止损价最小或订单注释字母序最靠前的那笔,外汇与贵金属杠杆交易风险高,下单前务必人工复核返回值。
class="type">int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE class="kw">property) { class="type">int index=class="num">0; COrder* min_order=NULL; class="type">int total=list_source.Total(); if(total== class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++){ COrder* order=list_source.At(i); class="type">class="kw">double order1_prop=order.GetProperty(class="kw">property); min_order=list_source.At(index); class="type">class="kw">double order2_prop=min_order.GetProperty(class="kw">property); if(CompareValues(order1_prop,order2_prop,LESS)) index=i; } class="kw">return index; } class="type">int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_STRING class="kw">property) { class="type">int index=class="num">0; COrder* min_order=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++){ COrder* order=list_source.At(i); class="type">class="kw">string order1_prop=order.GetProperty(class="kw">property); min_order=list_source.At(index); class="type">class="kw">string order2_prop=min_order.GetProperty(class="kw">property); if(CompareValues(order1_prop,order2_prop,LESS)) index=i; } class="kw">return index; }
「用宏和枚举给历史数据检索打底」
写 EA 或脚本抓账户历史时,先铺一层宏能少写很多重复字。下面这组定义把语言、函数名前缀、历史请求截止时间一次性固化:COUNTRY_LANG 存界面语言,DFUN 在日志里自动拼出「函数名: 」前缀,END_TIME 设成 D'31.12.3000 23:59:59',基本等于让 HistorySelect 拉到账户存在的全部记录。 检索逻辑离不开比较算子。ENUM_COMPARER_TYPE 枚举列了 6 种:EQUAL、MORE、LESS、NO_EQUAL、EQUAL_OR_MORE、EQUAL_OR_LESS,对应等于、大于、小于、不等于、大于等于、小于等于。实际筛成交或持仓时,按这个枚举分支写判断,比散落各处的 > < 符号好维护。 时间维度单独给了 ENUM_SELECT_BY_TIME:SELECT_BY_TIME_OPEN 按开仓时间、SELECT_BY_TIME_CLOSE 按平仓时间、SELECT_BY_TIME_OPEN_MSC 按含毫秒的开仓时间。外汇和贵金属杠杆高、跳空常见,用毫秒级开盘时间排序可避免跨日重叠导致的统计偏差,但任何历史回测结论都只是概率倾向,实盘仍可能因滑点偏离。 开 MT5 新建一个 include 头文件,把这段直接贴进去编译,若 DFUN 在 Experts 日志里正常输出「函数名: 」就说明宏替代生效,可接着往检索函数里接。
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">// 请求帐户历史数据的最终数据 enum ENUM_COMPARER_TYPE { EQUAL, class=class="str">"cmt">// 等于 MORE, class=class="str">"cmt">// 大于 LESS, class=class="str">"cmt">// 小于 NO_EQUAL, class=class="str">"cmt">// 不等于 EQUAL_OR_MORE, class=class="str">"cmt">// 大于等于 EQUAL_OR_LESS class=class="str">"cmt">// 小于等于 }; enum ENUM_SELECT_BY_TIME { SELECT_BY_TIME_OPEN, class=class="str">"cmt">// 按开盘时间 SELECT_BY_TIME_CLOSE, class=class="str">"cmt">// 按收盘时间 SELECT_BY_TIME_OPEN_MSC, class=class="str">"cmt">// 按开盘毫秒时间 };
把历史订单塞进一个类里管
上面这段声明只是收尾,真正干活的是 CHistoryCollection 这个类。它内部用 CArrayObj 存全部历史订单与成交,m_list_all_orders 就是那个总池子,外部调 GetList() 能原样拿回指针,不拷贝、不过滤。 私有成员里有两个容易被忽略的计数器:m_index_order 和 m_index_deal 记录上一次从终端历史拉取时写到的位置;m_delta_order / m_delta_deal 则是本次与上次的数量差。靠这几个 int,类可以只增量处理新历史,而不是每次从头扫一遍。外汇和贵金属品种在 MT5 里成交刷新频繁,这种差分思路能省掉不少冗余遍历,但高频回测时仍要警惕历史表锁带来的延迟。 GetListByTime() 给了按时间窗切历史的入口,默认 begin_time=0、end_time=0 表示不裁剪。配合前面 SELECT_BY_TIME_CLOSE_MSC 那个枚举,说明选择粒度能精确到收盘毫秒——这对剥头皮策略做滑点归因可能有用。 头文件引用也值得照抄:ArrayObj.mqh 是容器基底,DELib.mqh 与 Objects 下的 HistoryOrder / HistoryPending / HistoryDeal 是自定义对象层,Select.mqh 单独管筛选逻辑。在 MT5 里建个相同目录结构,把这几行 include 贴进 ea,编译过即说明依赖齐了。
SELECT_BY_TIME_CLOSE_MSC, class=class="str">"cmt">// 按收盘毫秒时间 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "..\DELib.mqh" class="macro">#include "..\Objects\HistoryOrder.mqh" class="macro">#include "..\Objects\HistoryPending.mqh" class="macro">#include "..\Objects\HistoryDeal.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "Select.mqh" class="macro">#include "..\Objects\HistoryOrder.mqh" class="macro">#include "..\Objects\HistoryPending.mqh" class="macro">#include "..\Objects\HistoryDeal.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 历史订单和成交集合 | class=class="str">"cmt">//+------------------------------------------------------------------+ 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">// 与过去的检查相比,成交数量的差值 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,