
用于轻松快速开发 MetaTrader 程序的函数库(第三部分)。 市价订单和仓位的集合,搜索和排序
内容
布局搜索
引擎(Engine)基准对象是函数库的核心
激活市价订单和仓位的对象
激活市价订单和仓位的集合
下一步是什么
在 本系列文章的第一部分 当中,我们曾创建了一个大型跨平台库,简化了 MetaTrader 5 和 MetaTrader 4 平台的程序开发。
在 第二部分 当中,我们再次深入函数库的开发,并实现了历史订单和成交的集合。
在此,我们继续创建一个用于便利地选择和排序订单、成交和仓位集合列表的类,实现名为引擎(Engine)的基准函数对象,并向函数库中添加市价订单和仓位的集合。
此刻,已经出现了合适的数据存储结构。 在创建各种对象类型的集合时,我们将遵循它:
创建单个 Engine 对象来存储和管理集合,以及在程序和函数库之间交换数据。 Engine 将成为整个函数库的基准对象。 基于该函数库的程序会引用它来获取数据。 此外,它也在为整个函数库自动化进行积累。
布局搜索
为了轻松便捷地使用来自函数库集合的数据,我们将根据请求实现便利地数据搜索、排序和显示。 若要实现这一点,我们要创建一个特殊的类,并将其命名为 CSelect。
所有数据请求都要经由它传递。
在 Collections 函数库文件夹中,创建新的 CSelect 类。 而它无需设置基类。 完成 MQL 向导操作后,将在 Collections 文件夹中生成新的 Select.mqh 文件:
//+------------------------------------------------------------------+ //| Select.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CSelect { private: public: CSelect(); ~CSelect(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CSelect::CSelect() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CSelect::~CSelect() { } //+------------------------------------------------------------------+
若要执行搜索,先设置其全部模式。 为此,请在搜索期间创建描述对象比较模式的枚举。 枚举将在 Defines.mqh 文件中创建:
//+------------------------------------------------------------------+ //| Defines.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" //+------------------------------------------------------------------+ //| 宏替代 | //+------------------------------------------------------------------+ #define COUNTRY_LANG ("Russian") // 国家语言 #define DFUN (__FUNCTION__+": ") // "函数描述" #define END_TIME (D'31.12.3000 23:59:59') // 请求帐户历史数据的最终数据 //+------------------------------------------------------------------+ //| 搜索 | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 搜索数据 | //+------------------------------------------------------------------+ enum ENUM_COMPARER_TYPE { EQUAL, // 等于 MORE, // 大于 LESS, // 小于 NO_EQUAL, // 不等于 EQUAL_OR_MORE, // 大于等于 EQUAL_OR_LESS // 小于等于 }; //+------------------------------------------------------------------+
在来自 标准库的 CSelect 类文件当中,会连接指向对象实例的动态指针列表的类,COrder 类和 DELib.mqh 服务函数库(以及 Defines.mqh 文件)。 此外,在全局层次上声明整个函数库可用的特殊存储对象。 它是存储在排序期间创建的列表副本。 如果新创建的列表未附加到存储对象,则应在对象消失后删除它们。 这意味着我们需要分配额外的资源来跟踪我们所需列表的位置、时间和原因,而此操作可能会导致逻辑链丢失,以及由于对象未删除而产生的内存泄漏。 如果我们将对象附加到列表中,则跟踪由终端子系统执行,终端子系统总是会及时删除存储列表及其内容。
为了进行比较,我们需要创建这样一种方法。 我们声明一个用于比较两个数值的模板静态方法。//+------------------------------------------------------------------+ //| Select.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "..\Objects\Order.mqh" //+------------------------------------------------------------------+ //| 存储列表 | //+------------------------------------------------------------------+ CArrayObj ListStorage; // 用于存储已排序集合列表的存储对象 //+------------------------------------------------------------------+ //| 用于为符合标准的对象进行排序的类 | //+------------------------------------------------------------------+ class CSelect { private: //--- 比较两个数值的方法 template<typename T> static bool CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode); public: }; //+------------------------------------------------------------------+
实现比较方法:
//+------------------------------------------------------------------+ //| 比较两个数值的方法 | //+------------------------------------------------------------------+ template<typename T> bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode) { return ( mode==EQUAL && value1==value2 ? true : mode==NO_EQUAL && value1!=value2 ? true : mode==MORE && value1>value2 ? true : mode==LESS && value1<value2 ? true : mode==EQUAL_OR_MORE && value1>=value2 ? true : mode==EQUAL_OR_LESS && value1<=value2 ? true : false ); } //+------------------------------------------------------------------+
两个数值拥有相同类型,并将比较模式传递给该方法。
接着,依据所应用的方法进行简单比较(等于/不等于/大于/小于/等于,或大于等于,或小于等于)并返回结果。
现在我们来创建几种搜索列表的方法。 在 CSelect 类的公有(public)部分中,声明了三个静态方法,用于按照给定标准搜索订单:
//+------------------------------------------------------------------+ //| 用于为符合标准的对象进行排序的类 | //+------------------------------------------------------------------+ class CSelect { private: //--- 二值比较方法 template<typename T> static bool CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode); public: //--- 返回订单列表,其中之一(1)整数,(2)实数,和(3)符合指定标准的字符串属性 static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode); }; //+------------------------------------------------------------------+
我们在类的实体外立即实现它们:
//+------------------------------------------------------------------+ //| 返回订单列表的整数型属性 | //| 订单需要符合指定条件 | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode) { if(list_source==NULL) return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) return NULL; list.FreeMode(false); ListStorage.Add(list); int total=list_source.Total(); for(int i=0; i<total; i++) { COrder *order=list_source.At(i); if(!order.SupportProperty(property)) continue; long order_prop=order.GetProperty(property); if(CompareValues(order_prop,value,mode)) list.Add(order); } return list; } //+------------------------------------------------------------------+ //| 返回订单列表的实数型属性 | //| 订单需要符合指定条件 | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode) { if(list_source==NULL) return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) return NULL; list.FreeMode(false); ListStorage.Add(list); for(int i=0; i<list_source.Total(); i++) { COrder *order=list_source.At(i); if(!order.SupportProperty(property)) continue; double order_prop=order.GetProperty(property); if(CompareValues(order_prop,value,mode)) list.Add(order); } return list; } //+------------------------------------------------------------------+ //| 返回订单列表的字符串型属性 | //| 订单需要符合指定条件 | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode) { if(list_source==NULL) return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) return NULL; list.FreeMode(false); ListStorage.Add(list); for(int i=0; i<list_source.Total(); i++) { COrder *order=list_source.At(i); if(!order.SupportProperty(property)) continue; string order_prop=order.GetProperty(property); if(CompareValues(order_prop,value,mode)) list.Add(order); } return list; } //+------------------------------------------------------------------+
我们来深入观察使用字符串作为搜索条件的示例:
- 该方法接收指向集合列表的指针,新列表将取决于搜索的属性和比较模式。 该属性应符合搜寻标准。
- 之后,检查列表的有效性,如果无效则返回 NULL。
- 如果检查通过,则创建新的列表对象,并为其设置手工内存管理标志。 如果不这样做,那么当删除该列表对象时,所有指向集合内存储的订单对象的指针也会一并删除,而这是不可接受的。 您可以在动态指针列表的帮助中找到详细信息,特别是此方法。
- 接下来,将新创建的列表添加到存储列表,并开始在循环中对对象进行排序:
- 从列表中获取一笔订单。 如果它不支持搜索中指定的属性,则跳过它并选择下一个。
- 接下来,根据指定模式(大于/小于/等于,等等),取订单属性与所传递的属性进行比较。 如果满足比较标准,则将此订单添加到新创建的列表当中。
- 在循环末尾,列表将返回给调用程序。
添加另外六种搜索方法,并返回含有指定属性最大和最小值的订单索引:
//+------------------------------------------------------------------+ //| 用于为符合标准的对象进行排序的类 | //+------------------------------------------------------------------+ class CSelect { private: //--- 二值比较方法 template<typename T> static bool CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode); public: //--- 返回订单列表,其中之一(1)整数,(2)实数,和(3)符合指定标准的字符串属性 static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByOrderProperty(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode); //--- 返回含有(1)整数,(2)实数,和(3)字符串属性最大值的订单索引 static int FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property); static int FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property); static int FindOrderMax(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property); //--- 返回含有(1)整数,(2)实数,和(3)字符串属性最小值的订单索引 static int FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property); static int FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property); static int FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property); }; //+------------------------------------------------------------------+
和它们的实现:
//+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有整数属性最大值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_INTEGER property) { if(list_source==NULL) return WRONG_VALUE; int index=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; } //+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有实数属性最大值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_DOUBLE property) { if(list_source==NULL) return WRONG_VALUE; int index=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); 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; } //+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有字符串属性最大值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMax(CArrayObj *list_source,ENUM_ORDER_PROP_STRING property) { if(list_source==NULL) return WRONG_VALUE; int index=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); string order1_prop=order.GetProperty(property); max_order=list_source.At(index); string order2_prop=max_order.GetProperty(property); if(CompareValues(order1_prop,order2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+
我们来深入观察搜索含有最大字符串值的示例:
- 该方法接收指向集合列表的指针,和属性,按该属性搜索含最大值的订单。
- 此后,检查列表的有效性,如果无效,则返回 WRONG_VALUE( - 1)。
- 声明含最大值的订单索引,以零初始化它,并创建一个空订单对象,用于存储比较值。
- 接着,在循环里自第二笔订单顺序比较集合列表:
- 按循环索引从订单里获取目标数值,从订单里获取 'index' 索引的目标值,并比较两个获得的数值。 如果第一个订单的目标值(带有循环索引)超过第二个订单(带有 'index' 索引),将含有最大值的订单索引赋值给 'index' 变量。
- 直至循环完成后,'index' 变量将含有目标数值最高的订单索引。 将其返回给调用程序。
返回含有指定属性最小值的订单索引的方法与此类似:
//+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有整数属性最小值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_INTEGER property) { int index=0; COrder* min_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); min_order=list_source.At(index); long order2_prop=min_order.GetProperty(property); if(CompareValues(order1_prop,order2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有实数属性最小值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_DOUBLE property) { int index=0; COrder* min_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); double order1_prop=order.GetProperty(property); min_order=list_source.At(index); double order2_prop=min_order.GetProperty(property); if(CompareValues(order1_prop,order2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| 返回列表中的订单索引 | //| 含有字符串属性最小值 | //+------------------------------------------------------------------+ int CSelect::FindOrderMin(CArrayObj* list_source,ENUM_ORDER_PROP_STRING property) { int index=0; COrder* min_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); string order1_prop=order.GetProperty(property); min_order=list_source.At(index); string order2_prop=min_order.GetProperty(property); if(CompareValues(order1_prop,order2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+
现在我们可将按时间和指定标准为集合列表排序的方法添加到历史订单集合类中。
首先,将按时间排序的选项添加到 Defines.mqh 文件中:
//+------------------------------------------------------------------+ //| Defines.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" //+------------------------------------------------------------------+ //| 宏替代 | //+------------------------------------------------------------------+ #define COUNTRY_LANG ("Russian") // 国家语言 #define DFUN (__FUNCTION__+": ") // "函数描述" #define END_TIME (D'31.12.3000 23:59:59') // 请求帐户历史数据的最终数据 //+------------------------------------------------------------------+ //| 搜索 | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 数据搜索和排序 | //+------------------------------------------------------------------+ enum ENUM_COMPARER_TYPE { EQUAL, // 等于 MORE, // 大于 LESS, // 小于 NO_EQUAL, // 不等于 EQUAL_OR_MORE, // 大于等于 EQUAL_OR_LESS // 小于等于 }; //+------------------------------------------------------------------+ //| 按时间选择的可能选项 | //+------------------------------------------------------------------+ enum ENUM_SELECT_BY_TIME { SELECT_BY_TIME_OPEN, // 按开盘时间 SELECT_BY_TIME_CLOSE, // 按收盘时间 SELECT_BY_TIME_OPEN_MSC, // 按开盘毫秒时间 SELECT_BY_TIME_CLOSE_MSC, // 按收盘毫秒时间 }; //+------------------------------------------------------------------+
将 CSelect 类包含在 HistoryCollection.mqh 文件中。 为此,将服务函数包含字符串替换为 CSelec t类文件包含:
//+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "..\DELib.mqh" #include "..\Objects\HistoryOrder.mqh" #include "..\Objects\HistoryPending.mqh" #include "..\Objects\HistoryDeal.mqh" //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "Select.mqh" #include "..\Objects\HistoryOrder.mqh" #include "..\Objects\HistoryPending.mqh" #include "..\Objects\HistoryDeal.mqh" //+------------------------------------------------------------------+
现在,我们已用 CSelect 类文件替换了服务函数。 我们已将 Order.mqh 包含到 Select.mqh 中,而服务函数文件已包含在 Order.mqh 文件中。
在 CHistoryCollection 类的公有部分中,声明从集合中按指定时间自日期范围内选择订单的方法,而在私有部分中,添加 COrder 抽象订单类用作从样本订单里搜索数值://+------------------------------------------------------------------+ //| 历史订单和成交集合 | //+------------------------------------------------------------------+ class CHistoryCollection { private: CArrayObj m_list_all_orders; // 所有历史订单和成交的列表 COrder m_order_instance; // 按属性搜索订单对象 bool m_is_trade_event; // 交易事件标志 int m_index_order; // 将最后一笔订单的索引添加到取自终端历史列表(MQL4,MQL5)的集合中 int m_index_deal; // 将最后一笔成交的索引添加到取自终端历史列表(MQL5)的集合中 int m_delta_order; // 与过去的检查相比,订单数量的差值 int m_delta_deal; // 与过去的检查相比,成交数量的差值 public: //--- 原样返回完整的集合列表 CArrayObj *GetList(void) { return &m_list_all_orders; } //--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(const datetime begin_time=0,const datetime end_time=0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); //--- 构造函数 CHistoryCollection(); //--- 更新订单列表,填写新订单数量,并设置交易事件标志 void Refresh(void); }; //+------------------------------------------------------------------+
在实现自日期范围内从集合中选择订单的方法之前,在 Order.mqh 文件中的 COrder 抽象订单类里添加放置整数型,实数型和字符串型属性的方法,(在这种情况下,输出整数型属性的方法需要将参数添加到采样时间数据中):
public: //--- 设置 (1) 整数型,(2) 实数型,和 (3) 字符串型订单属性 void SetProperty(ENUM_ORDER_PROP_INTEGER property,long value) { m_long_prop[property]=value; } void SetProperty(ENUM_ORDER_PROP_DOUBLE property,long value) { m_long_prop[property]=value; } void SetProperty(ENUM_ORDER_PROP_STRING property,long value) { m_long_prop[property]=value; } //--- 从属性数组里返回 (1) 整数型,(2) 实数型,和 (3) 字符串型订单属性 long GetProperty(ENUM_ORDER_PROP_INTEGER property) const { return m_long_prop[property]; } double GetProperty(ENUM_ORDER_PROP_DOUBLE property) const { return m_double_prop[this.IndexProp(property)]; } string GetProperty(ENUM_ORDER_PROP_STRING property) const { return m_string_prop[this.IndexProp(property)]; } //--- 返回订单支持该属性的标志 virtual bool SupportProperty(ENUM_ORDER_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_ORDER_PROP_DOUBLE property) { return true; } virtual bool SupportProperty(ENUM_ORDER_PROP_STRING property) { return true; } //--- 将 COrder 对象与所有可能的属性相互比较 virtual int Compare(const CObject *node,const int mode=0) const; //+------------------------------------------------------------------+
在 HistoryCollection.mqh 文件中,实现在日期范围内从集合中选择订单的方法:
//+------------------------------------------------------------------+ //| 自集合里选择订单 | //| 从 begin_time 至 end_time | //+------------------------------------------------------------------+ CArrayObj *CHistoryCollection::GetListByTime(const datetime begin_time=0,const datetime end_time=0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE) { ENUM_ORDER_PROP_INTEGER 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")); return NULL; } datetime begin=begin_time,end=(end_time==0 ? END_TIME : end_time); if(begin_time>end_time) begin=0; list.FreeMode(false); ListStorage.Add(list); //--- m_order_instance.SetProperty(property,begin); int index_begin=m_list_all_orders.SearchGreatOrEqual(&m_order_instance); if(index_begin==WRONG_VALUE) return list; m_order_instance.SetProperty(property,end); int index_end=m_list_all_orders.SearchLessOrEqual(&m_order_instance); if(index_end==WRONG_VALUE) return list; for(int i=index_begin; i<=index_end; i++) list.Add(m_list_all_orders.At(i)); return list; } //+------------------------------------------------------------------+
那么,我们在这里做什么呢?
- 该方法接收所需历史记录的范围开始时间</ s0>,范围结束时间,并从 ENUM_SELECT_BY_TIME 枚举中选择范围日期的模式。
- 所需搜索和比较的订单属性设置为按时间列表排序的。 如果已按开盘时间排序,则搜索属性将会是开盘时间。 如果已按收盘时间排序,则会比较订单的收盘时间,以此类推。
- 而 新列表 随后被创建。 它将包含与日期范围标准相对应的订单,并最终返回给调用程序。
- 然后检查范围开始和结束日期。
- 如果所传递的范围结束日期为零,则设置最大未来日期,然后检查范围开始日期。 如果它超出范围结束日期,则将最早的日期设置为范围开始日期。 在此情况下,如果未正确设置日期,列表提取的数据则会从帐户历史记录的开始直到范围结束。 较早的日期被视为范围开始,而更接近当前时间的日期被视为范围结束。
- 然后,为新创建的列表设置手工存储器管理(如上所述)的标志,并将列表附加到存储对象里。
- 接下来,将初始日期设置为样本订单用于在集合列表中搜索,并使用从 CObjec 父类继承的 COrder 类当中的 SearchGreatOrEqual() 方法以初始日期搜索订单索引。
- 如果未发现索引,这意味着指定日期之后没有订单,并返回空列表。
- 接下来,搜索结束日期时也是如此:搜索中使用的结束日期设置为样本,使用 SearchLessOrEqual() 方法执行搜索结束日期的订单索引。 如果未找到索引,则表示没有早于目标日期的订单,并返回空列表。
- 接着,位于范围内的所有订单都在循环中被添加到列表里,从开始日期订单索引到结束日期订单索引,并填充将要返回给调用程序的列表。
在公有部分中,声明按照所选符合比较标准的整数型、实数型和字符串型属性返回列表的方法:
//+------------------------------------------------------------------+ //| 历史订单和成交集合 | //+------------------------------------------------------------------+ class CHistoryCollection { private: CArrayObj m_list_all_orders; // 所有历史订单和成交的列表 COrder m_order_instance; // 按属性搜索的订单对象 bool m_is_trade_event; // 交易事件标志 int m_index_order; // 将最后一笔订单的索引添加到取自终端历史列表(MQL4,MQL5)的集合中 int m_index_deal; // 将最后一笔成交的索引添加到取自终端历史列表(MQL5)的集合中 int m_delta_order; // 与过去的检查相比,订单数量的差值 int m_delta_deal; // 与过去的检查相比,成交数量的差值 public: //--- 原样返回完整的集合列表 CArrayObj *GetList(void) { return &m_list_all_orders; } //--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(const datetime begin_time=0,const datetime end_time=0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); //--- 返回按照选择的(1)整数型,(2)实数型,和(3)字符串型属性符合比较标准的列表 CArrayObj* GetList(ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } CArrayObj* GetList(ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } //--- 构造函数 CHistoryCollection(); //--- 更新订单列表,填写新订单数量,并设置交易事件标志 void Refresh(void); }; //+------------------------------------------------------------------+
该方法接收订单的目标属性,并按照比较模式(大于/小于/等于/不等于/大于等于或小于等于)比较数值。 在如前所述 CSelect 类方法的帮助下,返回按所需属性、数值和比较方法排序的列表。
我们来测试使用不同的方法接收所需的列表。
用来自第二部分中的测试 EA TestDoEasyPart02.mq5,将其保存在 MQL5\Experts\TestDoEasy 下的新 Part03 子文件夹中,并取名为 TestDoEasyPart03_1.mq5。 将选择日期范围的开始和结束添加到其输入参数,并更改 OnInit() 处理程序中的代码,我们打算在此请求日期范围内的历史记录:
//+------------------------------------------------------------------+ //| TestDoEasyPart03_1.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //--- 包含 #include <DoEasy\Collections\HistoryCollection.mqh> //--- 枚举 enum ENUM_TYPE_ORDERS { TYPE_ORDER_MARKET, // 市价订单 TYPE_ORDER_PENDING, // 挂单 TYPE_ORDER_DEAL // 成交 }; //--- 输入参数 input ENUM_TYPE_ORDERS InpOrderType = TYPE_ORDER_DEAL; // 显示类型: input datetime InpTimeBegin = 0; // 所需范围的开始日期 input datetime InpTimeEnd = END_TIME; // 所需范围的结束日期 //--- 全局变量 CHistoryCollection history; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 更新历史记录 history.Refresh(); //--- 获取日期范围内的集合列表 CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE); if(list==NULL) { Print("Could not get collection list"); return INIT_FAILED; } int total=list.Total(); for(int i=0;i<total;i++) { //--- 从列表中获取订单 COrder* order=list.At(i); if(order==NULL) continue; //--- 如果此为成交 if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL) order.Print(); //--- 如果此为历史市价单 if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET) order.Print(); //--- 如果此为被删除的挂单 if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING) order.Print(); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能系统即时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
现在,我们使用 GetListByTime() 方法获取按照指定日期范围选出的列表,而不是完整列表。 编译并启动 EA,使用默认设置。 整个帐户历史记录的所有成交都显示在流水日志中:
按 F7 并在设置中设定所需范围的结束日期。 就个人而言,我输入了帐户历史记录,确定何时发生了充值,定义了下一笔成交的日期(帐户充值后发生的第一笔成交)
并选择了该范围,以便第一笔成交在此范围之外:2018.01.22 - 2018.02.01。
结果就是,在流水日志中只显示了一笔成交(账户充值):
现在我们将 TestDoEasyPart03_1.mq5 EA 保存为 TestDoEasyPart03_2mq5。 删除输入并修改接收成交数据的方式:
//+------------------------------------------------------------------+ //| TestDoEasyPart03_2.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //--- 包含 #include <DoEasy\Collections\HistoryCollection.mqh> //--- 全局变量 CHistoryCollection history; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 更新历史记录 history.Refresh(); //--- 仅接受集合清单中的成交 CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); //--- 按照余额操作对获得的列表进行排序 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,DEAL_TYPE_BALANCE,EQUAL); if(list==NULL) { Print("Could not get collection list"); return INIT_FAILED; } int total=list.Total(); for(int i=0;i<total;i++) { //--- 从列表中获取订单 COrder* order=list.At(i); if(order==NULL) continue; order.Print(); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能系统即时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
首先,获取所有成交的列表(高亮显示列表中的成交类型的订单状态),并按照“余额操作”类型对获得的列表进行排序。 在全部两种情况下都使用等于比较模式。
所得结果,流水日志中仅显示“余额充值”操作:
在前面的示例中,我们必须查看终端帐户历史记录选项卡中的成交范围来显示余额操作,在此我们可立即获得按照所需条件对列表进行排序后的结果。
获得所需数据的任何其他方式遵循相同的原则。 例如,为了获得相同的“余额充值”,我们可以找到最多盈利和最低利润成交的索引。
TestDoEasyPart03_3.mq5 测试 EA 中的示例:
//+------------------------------------------------------------------+ //| TestDoEasyPart03_3.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //--- 包含 #include <DoEasy\Collections\HistoryCollection.mqh> //--- 全局变量 CHistoryCollection history; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 更新历史记录 history.Refresh(); //--- 仅接受集合清单中的成交 CArrayObj* list=history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); if(list==NULL) { Print(TextByLanguage("Не удалось получить список","Could not get list")); return INIT_FAILED; } //--- 获取最多盈利的成交索引(第一次余额充值) int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT); if(index!=WRONG_VALUE) { //--- 按索引从列表中获取成交 COrder* order=list.At(index); if(order!=NULL) order.Print(); } else Print(TextByLanguage("Не найден индекс ордера с максимальным значением профита","Order index with maximum profit value not found")); //--- 获得利润最低的成交索引 index=CSelect::FindOrderMin(list,ORDER_PROP_PROFIT); if(index!=WRONG_VALUE) { //--- 按索引从列表中获取成交 COrder* order=list.At(index); if(order!=NULL) order.Print(); } else Print(TextByLanguage("Не найден индекс ордера с минимальным значением профита","Order index with minimum profit value not found")); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能系统即时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
完成后,流水日志中会显示两笔成交 — 一笔是利润最多(余额充值),另一笔是利润最少的成交。
引擎(Engine)基准对象是函数库的核心
协同函数库工作的自定义程序应将数据发送到函数库,并从其接收数据。 为了达成这一点,将一个类连接到程序并累积所有可能的动作,以便在函数库和程序之间进行通信更方便。 当然,该类应该具备在后台工作的所有可能的服务函数,并且不需要用户进行任何代价高昂的动作。 因此,我们将创建一个基类作为函数库基础,并将其命名为 Engine。
在函数库的根文件夹中,根据基类 CObject 和 连接历史集合类创建新的 CEngine 类:
//+------------------------------------------------------------------+ //| Engine.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include "Collections\HistoryCollection.mqh" //+------------------------------------------------------------------+ //| 函数库基础类 | //+------------------------------------------------------------------+ class CEngine : public CObject { private: //--- 历史订单和成交的集合 CHistoryCollection m_history; public: CEngine(); ~CEngine(); }; //+------------------------------------------------------------------+ //| CEngine 构造函数 | //+------------------------------------------------------------------+ CEngine::CEngine() { } //+------------------------------------------------------------------+ //| CEngine 析构函数 | //+------------------------------------------------------------------+ CEngine::~CEngine() { } //+------------------------------------------------------------------+
我们在测试 EA 中处理历史订单集合时执行的所有操作都是在 OnInit()处理程序中执行的。 换言之,它们在启动 EA、重新编译或更改其参数时仅执行一次。 对于快速检查这足矣了,但在工作程序中是不可接受的。 所以我们开始理清一切。
首先,在类的公有部分创建 OnTimer() 处理程序,并在类的实体外部创建其实现,以便更新所有集合://+------------------------------------------------------------------+ //| Engine.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include "Collections\HistoryCollection.mqh" //+------------------------------------------------------------------+ //| 函数库基础类 | //+------------------------------------------------------------------+ class CEngine : public CObject { private: //--- 历史订单和成交的集合 CHistoryCollection m_history; public: //--- 计时器 void OnTimer(void); CEngine(); ~CEngine(); }; //+------------------------------------------------------------------+ //| CEngine 构造函数 | //+------------------------------------------------------------------+ CEngine::CEngine() { } //+------------------------------------------------------------------+ //| CEngine 析构函数 | //+------------------------------------------------------------------+ CEngine::~CEngine() { } //+------------------------------------------------------------------+ //| CEngine 计时器 | //+------------------------------------------------------------------+ void CEngine::OnTimer(void) { } //+------------------------------------------------------------------+
我们来制作服务类(定时器计数器),因为很可能不同的事件需要不同的定时器延迟。 该类将计算所需的延迟时间,并为每个计时器声明一个单独的计数器实例。
首先,将新的宏替换添加到 Defines.mqh 文件中。 指定函数库定时器的频率和集合定时器计数器暂停的毫秒值,集合定时器计数器增量和更新历史订单与成交的计时器计数器 ID(在开发函数库时,您也许需要若干个独立 ID 的计数器)//+------------------------------------------------------------------+ //| 宏替代 | //+------------------------------------------------------------------+ #define COUNTRY_LANG ("Russian") // 国家语言 #define DFUN (__FUNCTION__+": ") // "函数描述" #define END_TIME (D'31.12.3000 23:59:59') // 请求帐户历史数据的结束日期 #define TIMER_FREQUENCY (16) // 函数库定时器的最小频率(以毫秒为单位) #define COLLECTION_PAUSE (250) // 订单和成交集合计时器暂停,以毫秒为单位 #define COLLECTION_COUNTER_STEP (16) // 订单和成交集合计时器计数器的增量 #define COLLECTION_COUNTER_ID (1) // 订单和成交集合计时器计数器 ID //+------------------------------------------------------------------+
在函数库的根文件夹中,创建新的Services文件夹,以及其中的新 CTimerCounter 类。 立即将 DELib.mqh 服务函数文件移至此文件夹。 这是它的正确位置。
替换 地址
//+------------------------------------------------------------------+ //| Order.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" #property strict // 对于 mql4 为必须 //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Object.mqh> #include "..\DELib.mqh" //+------------------------------------------------------------------+
以此 地址:
//+------------------------------------------------------------------+ //| Order.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" #property strict // 对于 mql4 为必须 //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Object.mqh> #include "..\Services\DELib.mqh" //+------------------------------------------------------------------+
现在我们来考察计时器计数器类。 这个类很简单。 那么我们来看看其列表并详述其操作:
//+------------------------------------------------------------------+ //| TimerCounter.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Object.mqh> #include "DELib.mqh" //+------------------------------------------------------------------+ //| 定时器计数器类 | //+------------------------------------------------------------------+ class CTimerCounter : public CObject { private: int m_counter_id; ulong m_counter; ulong m_counter_step; ulong m_counter_pause; public: //--- 返回等待完成标志 bool IsTimeDone(void); //--- 设置计数器参数 void SetParams(const ulong step,const ulong pause) { this.m_counter_step=step; this.m_counter_pause=pause; } //--- 返回计数器 ID virtual int Type(void) const { return this.m_counter_id; } //--- 比较计数器对象 virtual int Compare(const CObject *node,const int mode=0) const; //--- 构造函数 CTimerCounter(const int id); }; //+------------------------------------------------------------------+ //| CTimerCounter 构造函数 | //+------------------------------------------------------------------+ CTimerCounter::CTimerCounter(const int id) : m_counter(0),m_counter_step(16),m_counter_pause(16) { this.m_counter_id=id; } //+------------------------------------------------------------------+ //| CTimerCounter 返回暂停完成标志 | //+------------------------------------------------------------------+ bool CTimerCounter::IsTimeDone(void) { if(this.m_counter>=ULONG_MAX) this.m_counter=0; if(this.m_counter<this.m_counter_pause) { this.m_counter+=this.m_counter_step; return false; } this.m_counter=0; return true; } //+------------------------------------------------------------------+ //| 按 ID 比较 CTimerCounter 对象 | //+------------------------------------------------------------------+ int CTimerCounter::Compare(const CObject *node,const int mode=0) const { const CTimerCounter *counter_compared=node; int value_compared=counter_compared.Type(); int value_current=this.Type(); return(value_current>value_compared ? 1 : value_current<value_compared ? -1 : 0); return 0; } //+------------------------------------------------------------------+
由于我们已将 DELib.mqh 移至计数器类所在的同一文件夹,因此我们应该直接从同一文件夹中包含它。 Defines.mqh 已包含在 DELib.mqh 中,这意味着该类能看到所有宏替换。
- 在私有部分中声明了四个类成员变量:timer ID,timer counter,timer increment 和 pause。
- 设置所需计数器参数的方法位于公有部分。 该方法传递计时器 step 和 pause。 所传递的数值立即分配给类成员变量。
- 在类构造函数中,timer counter (0),timer step (16) 和 timer pause (16) 会在初始化列表中指定。 step 和 pause 参数允许定时器在没有延迟的情况下工作,同时等待达到 pause 持续时间。
- 由输入传递传递的数值被赋值给类构造函数实体中的计数器 ID。
返回暂停完成标志的方法布局方式很简单:
- 首先,系统检查变量计数器溢出。 如果该值超过可能的最大值,则计数器重置为零。
- 接下来,系统比较计时器计数器和暂停值。 如果计数器值小于暂停的值,则将增量添加到计数器并返回 “false”。
- 如果计数器值超过或等于暂停,则计数器复位,且返回计数器等待时间完成事件。
返回 Type() 计数器 ID 的方法是虚拟的。 在发布的 CObject 类中,有一个虚方法返回对象类型:
//--- 识别对象的方法 virtual int Type(void) const { return(0); }
假设此方法将在后代类中重新定义,并返回 CObject 后代类对象的 ID(若是 CObject 本身则返回 type=0)。 我们利用这个机会,通过重新定义虚方法返回计数器 ID:
virtual int Type(void) const { return this.m_counter_id; }
比较两个计数器对象的虚方法很简单:
//+------------------------------------------------------------------+ //| 按 ID 比较 CTimerCounter 对象 | //+------------------------------------------------------------------+ int CTimerCounter::Compare(const CObject *node,const int mode=0) const { const CTimerCounter *counter_compared=node; int value_compared=counter_compared.Type(); int value_current=this.Type(); return(value_current>value_compared ? 1 : value_current<value_compared ? -1 : 0); return 0; } //+------------------------------------------------------------------+
获取源对象的链接,获取其 ID,并获取当前计数器的 ID。 接着,通过大于/小于/等于返回简单比较的结果。
我们继续编写 CEngine 类。 将定时器计数器类包含在 Engine.mqh 文件中:
//+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include "Collections\HistoryCollection.mqh" #include "Services\TimerCounter.mqh" //+------------------------------------------------------------------+
添加定时器计数器的列表对象,以及按其 ID 返回列表中的计数器索引方法到私有部分,且计数器创建方法到公有部分(以便可以从外部访问该方法,并可以在程序中创建自定义计数器)。
在类构造函数中,初始化毫秒计时器,设置排序列表标志,创建更新历史订单和成交集合的计时器。 在类的析构函数中注销计时器://+------------------------------------------------------------------+ //| 函数库基础类 | //+------------------------------------------------------------------+ class CEngine : public CObject { private: //--- 定时器计数器列表 CArrayObj m_list_counters; //--- 历史订单和成交的集合 CHistoryCollection m_history; //--- 按 id 返回计数器索引 int CounterIndex(const int id) const; public: //--- 创建定时器计数器 void CreateCounter(const int counter_id,const ulong frequency,const ulong pause); //--- 计时器 void OnTimer(void); CEngine(); ~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::~CEngine() { ::EventKillTimer(); } //+------------------------------------------------------------------+
实现按照其 ID 返回计数器索引的方法:
//+------------------------------------------------------------------+ //| 按 ID 返回列表中的计数器索引 | //+------------------------------------------------------------------+ int CEngine::CounterIndex(const int id) const { int total=this.m_list_counters.Total(); for(int i=0;i<total;i++) { CTimerCounter* counter=this.m_list_counters.At(i); if(counter==NULL) continue; if(counter.Type()==id) return i; } return WRONG_VALUE; } //+------------------------------------------------------------------+
鉴于可能几乎没有多少计数器,我已经为数值搜索和比较安排了最简单的枚举。 如果在列表中找到具有此 ID 的计数器,则系统返回其在列表中的索引,否则,系统将返回 -1。
我们来考察创建定时器计数器的方法:
//+------------------------------------------------------------------+ //| 创建定时器计数器 | //+------------------------------------------------------------------+ void CEngine::CreateCounter(const int id,const ulong step,const ulong pause) { if(this.CounterIndex(id)>WRONG_VALUE) { ::Print(TextByLanguage("Ошибка. Уже создан счётчик с идентификатором ","Error. Already created counter with id "),(string)id); return; } m_list_counters.Sort(); CTimerCounter* counter=new CTimerCounter(id); if(counter==NULL) ::Print(TextByLanguage("Не удалось создать счётчик таймера ","Failed to create timer counter "),(string)id); counter.SetParams(step,pause); if(this.m_list_counters.Search(counter)==WRONG_VALUE) this.m_list_counters.Add(counter); else { string t1=TextByLanguage("Ошибка. Счётчик с идентификатором ","Error. Counter with ID ")+(string)id; string t2=TextByLanguage(", шагом ",", step ")+(string)step; string t3=TextByLanguage(" и паузой "," and pause ")+(string)pause; ::Print(t1+t2+t3+TextByLanguage(" уже существует"," already exists")); delete counter; } } //+------------------------------------------------------------------+
首先,检查传递给方法的计数器 ID。 如果此类 ID 已存在,则在流水日志中显示相应的消息并退出方法 — 具有此 ID 的计数器已存在。
由于搜索只能在已排序列表中进行,因此为列表设置排序标志,创建新计数器对象, 检查成功创建,及设置计数器的必需属性。
否则,将形成包含所有参数的消息,并显示在流水日志中。 然后计数器对象被删除,因为我们已经有类似的了。
最后验证新创建计数器的所有参数是否与现有计数器匹配目前是多余的 — 在方法的最开始检查 ID 能防止重复创建已存在 ID 对象。 我保留它以便应对未来变化。
为了让 CEngine 类知道何时处理交易情况,应该了解历史订单和成交数量的变化。 为此,将列表中新出现的 orders 和 deals 的返回方法添加到 CHistoryCollection 类:
//+------------------------------------------------------------------+ //| 历史订单和成交集合 | //+------------------------------------------------------------------+ class CHistoryCollection { private: CArrayObj m_list_all_orders; // 所有历史订单和成交的列表 COrder m_order_instance; // 按属性搜索的订单对象 bool m_is_trade_event; // 交易事件标志 int m_index_order; // 将最后一笔订单的索引添加到取自终端历史列表(MQL4,MQL5)的集合中 int m_index_deal; // 将最后一笔成交的索引添加到取自终端历史列表(MQL5)的集合中 int m_delta_order; // 与过去的检查相比,订单数量的差值 int m_delta_deal; // 与过去的检查相比,成交数量的差值 public: //--- 按时间自 begin_time 至 end_time 从集合中选择订单 CArrayObj *GetListByTime(const datetime begin_time=0,const datetime end_time=0, const ENUM_SELECT_BY_TIME select_time_mode=SELECT_BY_TIME_CLOSE); //--- 原样返回完整的集合列表 CArrayObj *GetList(void) { return &m_list_all_orders; } //--- 返回按照选择的(1)整数型,(2)实数型,和(3)字符串型属性符合比较标准的列表 CArrayObj *GetList(ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } CArrayObj *GetList(ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } CArrayObj *GetList(ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode); } //--- 返回(1)新订单,和(2)新成交的数量 int NewOrders(void) { return m_delta_order; } int NewDeals(void) { return m_delta_deal; } //--- 构造函数 CHistoryCollection(); //--- 更新订单列表,填写新订单数量,并设置交易事件标志 void Refresh(void); }; //+------------------------------------------------------------------+
这些方法只返回响应类成员变量的值。
当前在 CEngine 中,我们可以在类计时器中检查它们的状态:
//+------------------------------------------------------------------+ //| CEngine 计时器 | //+------------------------------------------------------------------+ void CEngine::OnTimer(void) { //--- 历史订单和成交集合的计时器 int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); if(counter.IsTimeDone()) { this.m_history.Refresh(); if(this.m_history.NewOrders()>0) { Print(DFUN,TextByLanguage("Изменилось количество исторических ордеров: NewOrders=","Number of historical orders changed: NewOrders="),this.m_history.NewOrders()); } if(this.m_history.NewDeals()>0) { Print(DFUN,TextByLanguage("Изменилось количество сделок: NewDeals=","Number of deals changed: NewDeals="),this.m_history.NewOrders()); } } } } //+------------------------------------------------------------------+
获取历史订单和成交集合定时器的计数器索引,通过索引获取指向定时器计数器的指针,检查计时器延迟时间的完成情况并更新集合(仅限最后添加的订单或成交,如果有的话)。
如果历史订单的数量已更改,则在流水日志中显示该消息。 成交也是如此。我们来制作一个简单的 EA 进行检查。 在 Experts\TestDoEasy\Part3 中,创建包含计时器的名为 TestDoEasyPart03_4.mq5 的 EA。 若要使用计时器创建 EA 的模板,请在 MQL 向导的第二页上勾选 OnTimer:
单击“下一步”直到向导的操作完成。 所得结果,创建了一个空的 EA 模板。 将函数库主体文件与它连接,创建函数库类对象,并在 EA 计时器中调用函数库定时器 。
//+------------------------------------------------------------------+ //| TestDoEasyPart03_4.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //--- 包含 #include <DoEasy\Engine.mqh> //--- 全局变量 CEngine engine; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能系统即时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| 计时器函数 | //+------------------------------------------------------------------+ void OnTimer() { engine.OnTimer(); } //+------------------------------------------------------------------+
这是在 EA 中需要完成的全部操作,目的是获取有关帐户历史记录变更的数据。
如果我们现在启动 EA,下挂挂单并将其删除,流水日志中会显示有关历史订单数量更改的条目。
如果我们开仓,流水日志中会出现两个条目:
- 关于订单数量的变化(开市价单已发送)和
- 关于成交数量的变化(市价单已被激活,并产生“入场”成交)。
如果我们平仓,流水日志中会再出现两个条目:
- 关于市价单平仓的出现
- 关于新成交的出现(市价单平仓已被激活,并产生“离场”成交)
在首次启动期间,帐户历史记录中的订单和成交变更消息将显示在流水日志中。 发生这种情况是因为函数库在第一次启动期间读取整个历史记录,会导致订单和成交的数量(函数库在首次启动期间对它们一无所知)与初始零值之间出现差异,而在遍历完整帐户期间计算的所有订单和成交的数量将等于其全部数量。 这不太方便,且不必要。 因此,我们需要消除这种虚假的数量变更信息。 我们可以通过两种方式做到这一点:
- 在首次启动期间,在流水日志中不显示任何内容
- 首次启动时显示帐户状态消息
在 CEngine 类的私有部分中,添加第一次启动的标志,以及检查和重置标志的方法:
//+------------------------------------------------------------------+ //| 函数库基础类 | //+------------------------------------------------------------------+ class CEngine : public CObject { private: CHistoryCollection m_history; // 历史订单和成交集合 CArrayObj m_list_counters; // 定时器计数器列表 bool m_first_start; // 首次启动标志 //--- 按 id 返回计数器索引 int CounterIndex(const int id) const; //--- 返回首次启动标志 bool IsFirstStart(void); public: //--- 创建定时器计数器 void CreateCounter(const int id,const ulong frequency,const ulong pause); //--- 计时器 void OnTimer(void); CEngine(); ~CEngine(); }; //+------------------------------------------------------------------+
并在类主体之外添加检查方法,和首次启动标志重置的实现:
//+------------------------------------------------------------------+ //| 返回首次启动标志,重置标志 | //+------------------------------------------------------------------+ bool CEngine::IsFirstStart(void) { if(this.m_first_start) { this.m_first_start=false; return true; } return false; } //+------------------------------------------------------------------+
此处一切都很简单:如果设置了标志,重置它并返回 'true',否则返回 'false'。
现在我们需要在初始化列表中设置标志,以便它始终可以被响应激活。
//+------------------------------------------------------------------+ //| CEngine 构造函数 | //+------------------------------------------------------------------+ CEngine::CEngine() : m_first_start(true) { ::EventSetMillisecondTimer(TIMER_FREQUENCY); this.m_list_counters.Sort(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE); } //+------------------------------------------------------------------+
一切都准备好了。 从现在开始,在首次程序启动期间不会出现与首次历史计算相关的不必要的帐户历史事件。
我们可以从 MQL5\Experts\TestDoEasy\Part2\TestDoEasyPart03_4.mq5 启动测试 EA 来确认这一点,并确保在首次启动期间没有关于向帐户历史记录添加订单和成交的消息显示在智能系统日志中。
激活市价订单和仓位的对象
我相信,现在是时候暂停向 CEngine 类添加功能,并开始实现对象以及市价单和仓位的集合。 完成实现后,我们将继续开发 Engine 基础对象的功能,因为此功能会影响帐户的历史记录及其当前状态。在函数库的 Objects 文件夹中,基于 COrder 函数库的抽象订单创建新的 CMarketPosition类 — 这是一个市价仓位对象:
单击“完成”后,将创建名为 MarketPosition.mqh 的类模板。 我们马上添加 包含 COrder 类:
//+------------------------------------------------------------------+ //| MarketPosition.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include "Order.mqh" //+------------------------------------------------------------------+ //| 市价仓位 | //+------------------------------------------------------------------+ class CMarketPosition : public COrder { private: public: CMarketPosition(); ~CMarketPosition(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketPosition::CMarketPosition() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketPosition::~CMarketPosition() { } //+------------------------------------------------------------------+
更改构造函数以便将仓位票据传递给它,在其初始化列表中设置父类(COrder)的“市价仓位”状态,并且将票据发送给它。 声明三个虚方法,返回仓位支持的整数型、实数型和字符串型属性的标志:
//+------------------------------------------------------------------+ //| 市价仓位 | //+------------------------------------------------------------------+ class CMarketPosition : public COrder { public: //--- 构造函数 CMarketPosition(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_POSITION,ticket) {} //--- 支持的仓位属性 (1) 实数型,(2) 整数型 virtual bool SupportProperty(ENUM_ORDER_PROP_INTEGER property); virtual bool SupportProperty(ENUM_ORDER_PROP_DOUBLE property); virtual bool SupportProperty(ENUM_ORDER_PROP_STRING property); }; //+------------------------------------------------------------------+
并在类的实体外添加这些方法的实现:
//+------------------------------------------------------------------+ //| 如果仓位支持所传递的 | //| 整数型属性返回 'true',否则返回 'false' | //+------------------------------------------------------------------+ bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_INTEGER property) { if(property==ORDER_PROP_TIME_CLOSE || property==ORDER_PROP_TIME_CLOSE_MSC || property==ORDER_PROP_TIME_EXP || property==ORDER_PROP_POSITION_BY_ID || property==ORDER_PROP_DEAL_ORDER || property==ORDER_PROP_DEAL_ENTRY || property==ORDER_PROP_CLOSE_BY_SL || property==ORDER_PROP_CLOSE_BY_TP #ifdef __MQL5__ || property==ORDER_PROP_TICKET_FROM || property==ORDER_PROP_TICKET_TO #endif ) return false; return true; } //+------------------------------------------------------------------+ //| 如果仓位支持所传递的 | //| 实数型属性返回 'true',否则返回 'false' | //+------------------------------------------------------------------+ bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_DOUBLE property) { if(property==ORDER_PROP_PRICE_CLOSE || property==ORDER_PROP_PRICE_STOP_LIMIT) return false; return true; } //+------------------------------------------------------------------+ //| 如果仓位支持所传递的 | //| 字符串型属性返回 'true',否则返回 'false' | //+------------------------------------------------------------------+ bool CMarketPosition::SupportProperty(ENUM_ORDER_PROP_STRING property) { if(property==ORDER_PROP_EXT_ID) return false; return true; } //+------------------------------------------------------------------+
此处的所有内容类似于在函数库描述的第二部分中讨论的创建历史订单和成交对象。
现在我们以类似的方式创建一个挂单对象。 我们根据 Objects 文件夹中 COrder 函数库的抽象订单创建一个新类 CMarketPending,并输入已熟悉的由 MQL 向导创建的类模板的变更:
//+------------------------------------------------------------------+ //| MarketPending.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include "Order.mqh" //+------------------------------------------------------------------+ //| 挂单 | //+------------------------------------------------------------------+ class CMarketPending : public COrder { public: //--- 构造函数 CMarketPending(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_PENDING,ticket) {} //--- 支持的订单属性 (1) 实数型,(2) 整数型 virtual bool SupportProperty(ENUM_ORDER_PROP_DOUBLE property); virtual bool SupportProperty(ENUM_ORDER_PROP_INTEGER property); }; //+------------------------------------------------------------------+ //| 如果订单支持所传递的 | //| 整数型属性返回 'true',否则返回 'false' | //+------------------------------------------------------------------+ bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_INTEGER property) { if(property==ORDER_PROP_PROFIT_PT || property==ORDER_PROP_DEAL_ORDER || property==ORDER_PROP_DEAL_ENTRY || property==ORDER_PROP_TIME_UPDATE || property==ORDER_PROP_TIME_CLOSE || property==ORDER_PROP_TIME_CLOSE_MSC || property==ORDER_PROP_TIME_UPDATE_MSC || property==ORDER_PROP_TICKET_FROM || property==ORDER_PROP_TICKET_TO || property==ORDER_PROP_CLOSE_BY_SL || property==ORDER_PROP_CLOSE_BY_TP ) return false; return true; } //+------------------------------------------------------------------+ //| 如果订单支持所传递的 | //| 实数型属性返回 'true',否则返回 'false' | //+------------------------------------------------------------------+ bool CMarketPending::SupportProperty(ENUM_ORDER_PROP_DOUBLE property) { if(property==ORDER_PROP_COMMISSION || property==ORDER_PROP_SWAP || property==ORDER_PROP_PROFIT || property==ORDER_PROP_PROFIT_FULL || property==ORDER_PROP_PRICE_CLOSE ) return false; return true; } //+------------------------------------------------------------------+
将“挂单”状态传递至类构造函数的初始化列表中的基类 COrder。
我们已完成了创建市价订单和仓位集合所需的对象开发。
激活的市价单和仓位的集合
在创建历史订单和成交集合时,我们要遵循的规则就是持续检查整个历史记录。 因此,我们仅在其数量发生变化时才向先前创建的列表添加单笔新订单和成交。 在使用市价仓位列表时,则应牢记完全不同的规则 — 每次即时报价时列表应该是相关的。
为达此要求:
- 跟踪挂单数量的变化,对冲户的活跃持仓数量(因为净持账户只有一笔持仓)和持仓量(净持仓量增加或减少,对冲仓位其一部分平仓),
- 确保在每次即时报价处更新每笔现存对冲持仓或单笔净持仓的数据,始终拥有相关的仓位状态数据。
记住最后一次即时报价处指定的数值,将它们与当前状态的相同数据进行比较,如果有变化,则更新仓位或重新创建整个列表。 幸运的是,列表并不大,重新创建并不需要花费太多时间。
我们开始吧。 在 Collections 函数库文件夹中,创建新类 CMarketCollection。 为此,右键单击 Collection 文件夹,然后选择“新建文件”。 在新打开的 MQL 向导中,选择“新类”并单击“下一步”。
输入类 CMarketCollection 的名称,然后单击“完成”。 则 MarketCollection.mqh 类模板被创建:
//+------------------------------------------------------------------+ //| MarketCollection.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMarketCollection { private: public: CMarketCollection(); ~CMarketCollection(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketCollection::CMarketCollection() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketCollection::~CMarketCollection() { } //+------------------------------------------------------------------+
我们来充实它。
首先,包括所有准备好的类,以及它们所需的市价订单和仓位集合、搜索的实现:
//+------------------------------------------------------------------+ //| MarketCollection.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "Select.mqh" #include "..\Objects\MarketPending.mqh" #include "..\Objects\MarketPosition.mqh" //+------------------------------------------------------------------+
在类的私有部分中,创建结构,指定用于存储其中所有先前提到的跟踪值的变量(订单和仓位的数量等),并使用此结构创建两个类成员变量,用来存储当前和以前的数据类型:
//+------------------------------------------------------------------+ //| MarketCollection.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| 包含文件 | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "Select.mqh" #include "..\Objects\MarketPending.mqh" #include "..\Objects\MarketPosition.mqh" //+------------------------------------------------------------------+ //| 历史订单和成交集合 | //+------------------------------------------------------------------+ class CMarketCollection { private: struct MqlDataCollection { long hash_sum_acc; // 账户上所有订单和仓位的哈希值 int total_pending; // 帐户上的挂单数量 int total_positions; // 帐户上的仓位数量 double total_volumes; // 帐户里总订单和仓位的总交易量 }; MqlDataCollection m_struct_curr_market; // 账户里当前市价单和仓位的数据 MqlDataCollection m_struct_prev_market; // 账户里前次市价单和仓位的数据 public: CMarketCollection(); ~CMarketCollection(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketCollection::CMarketCollection() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CMarketCollection::~CMarketCollection() { } //+------------------------------------------------------------------+
让我们详细阐述结构中的哈希值。
如果我们想要准确定义已发生的帐户事件,则也许会订单和仓位数量不足。 删除挂单也许会令帐户中的订单和仓位总数变化。 另一方面,可以激活挂单转变为仓位。 在此情况下,订单和仓位的总和保持不变(对冲账户和 MQL4)— 仓位数量增加,但订单数量减少。 结果就是,总数维持不变。 这不适合我们。
我们来考察总交易量。 放置/删除挂单 — 帐户的总交易量已更改,打仓、平仓或修改仓位 — 帐户的总交易量已更改。 此选项似乎适合,但激活挂单并不会更改总交易量。
那么,我们来看看另一个仓位属性 — 其变化时间的毫秒值:开一笔新仓会令总仓位改变时间,部分平仓会改变仓位时间,以及在净持账户里加仓亦会改变总仓位时间。准确定义帐户发生变更的最合适选项是什么? 票据+仓位变化时间。 我们来验证:
- 开仓 — 票据之和有变化+ 仓位变化时间之和有变化— 有变化
- 平仓 — 票据之和有变化+ 仓位变化时间之和有变化— 有变化
- 放置挂单 — 票据之和有变化+ 仓位变化时间之和无变化 — 有变化
- 删除挂单 — 票据之和有变化+ 仓位变化时间之和无变化— 有变化
- 激活挂单 — 票据之和无变化 + 仓位变化时间之和有变化— 有变化
- 部分平仓 — 票据之和有变化+ 仓位变化时间之和有变化— 有变化
- 加仓 — 票据之和无变化 + 仓位变化时间之和有变化— 有变化
在类的私有部分中,创建指向对象的指针动态列表,来作为市场挂单和仓位的集合列表。 此外,创建两个标志:帐户上的交易事件标志,和表示持仓量变化的标志 ,用于简化 CEngine 类中交易事件的识别,以及用于设置交易量变化值,新仓位数量,和挂单三个类成员变量。
在公有部分中,声明更新集合列表的方法,并在类的实体外部编写类构造函数实现:
//+------------------------------------------------------------------+ //| 市价订单和仓位集合 | //+------------------------------------------------------------------+ class CMarketCollection { private: struct MqlDataCollection { long hash_sum_acc; // 账户上所有订单和仓位的哈希值 int total_pending; // 帐户上的挂单数量 int total_positions; // 帐户上的仓位数量 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; // 新挂单数量 public: //--- 构造函数 CMarketCollection(void); //--- 更新挂单和仓位列表 void Refresh(void); }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+
重置交易事件和仓位交易量变化标志,并在类构造函数的初始化列表中重置交易量变化值。
在构造函数体中,按开仓时间设置市价订单和仓位列表的排序,重置帐户先前状态的所有变量结构,除了之前的哈希值— 将它设为 -1(用于识别首次启动)。
在类的私有部分添加将当前收集的帐户数据保存在先前数据结构中的方法,以便随后检查帐户中订单数量和仓位的变化。 在类的公有部分添加三个方法返回新挂单数量,新仓位数量和返回账户内发生交易事件标志的方法:
//+------------------------------------------------------------------+ //| 市价订单和仓位集合 | //+------------------------------------------------------------------+ class CMarketCollection { private: struct MqlDataCollection { long hash_sum_acc; // 账户上所有订单和仓位的哈希值 int total_pending; // 帐户上的挂单数量 int total_positions; // 帐户上的仓位数量 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; // 新挂单数量 //--- 将帐户数据状态的前次值替换为当前值 void SavePrevValues(void) { this.m_struct_prev_market=this.m_struct_curr_market; } public: //--- 返回(1)新挂单,(2)新仓位,(3)发生交易事件标志的数量 int NewOrders(void) const { return this.m_new_pendings; } int NewPosition(void) const { return this.m_new_positions; } bool IsTradeEvent(void) const { return this.m_is_trade_event; } //--- 构造函数 CMarketCollection(void); //--- 更新挂单和仓位列表 void Refresh(void); }; //+------------------------------------------------------------------+
我们来实现更新当前市场状态的方法:
//+------------------------------------------------------------------+ //| 更新订单列表 | //+------------------------------------------------------------------+ void CMarketCollection::Refresh(void) { ::ZeroMemory(this.m_struct_curr_market); this.m_is_trade_event=false; this.m_is_change_volume=false; this.m_new_pendings=0; this.m_new_positions=0; this.m_change_volume_value=0; m_list_all_orders.Clear(); #ifdef __MQL4__ int total=::OrdersTotal(); for(int i=0; i<total; i++) { if(!::OrderSelect(i,SELECT_BY_POS)) continue; 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) 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")); delete position; } } else { CMarketPending *order=new CMarketPending(ticket); if(order==NULL) 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")); delete order; } } } //--- MQ5 #else //--- 仓位 int total_positions=::PositionsTotal(); for(int i=0; i<total_positions; i++) { ulong ticket=::PositionGetTicket(i); if(ticket==0) continue; CMarketPosition *position=new CMarketPosition(ticket); if(position==NULL) continue; if(this.m_list_all_orders.InsertSort(position)) { this.m_struct_curr_market.hash_sum_acc+=(long)::PositionGetInteger(POSITION_TIME_UPDATE_MSC); this.m_struct_curr_market.total_volumes+=::PositionGetDouble(POSITION_VOLUME); this.m_struct_curr_market.total_positions++; } else { ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to list")); delete position; } } //--- 订单 int total_orders=::OrdersTotal(); for(int i=0; i<total_orders; i++) { ulong ticket=::OrderGetTicket(i); if(ticket==0) continue; CMarketPending *order=new CMarketPending(ticket); if(order==NULL) continue; if(this.m_list_all_orders.InsertSort(order)) { this.m_struct_curr_market.hash_sum_acc+=(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")); delete order; } } #endif //--- 首次启动 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,4); this.m_is_change_volume=(this.m_change_volume_value!=0 ? true : false); this.m_is_trade_event=true; this.SavePrevValues(); } } //+------------------------------------------------------------------+
在分析方法之前,我们小小的插句话:由于我们经常需要所有市价单和仓位的相关数据,我们可以在每次即时报价点清除列表,并用市场环境的数据填充。 再者,我们可以填写一次列表,之后仅更改可能变化的数据。 乍一看,似乎只更改变化数据会更快。 但要做到这一点,我们需要:
- 遍历市价订单和终端的仓位列表,并用它们填充函数库列表,
- 在每此即时报价点,查看终端内市价订单和仓位列表,获取变化的数据,在函数库列表中查找具有相同票据的订单和仓位,并更新现有数据,
- 如果删除订单或平仓,则将其从函数库列表中删除。
这似乎比简单地清理函数库列表,并在一个循环中按市价订单和终端内仓位填充它更昂贵。
因此,我们遵循一种更简单的方式:清除列表并再次填充。 当然,没有什么能阻止我们在现有的函数库列表中尝试涉及数据搜索和更新的方法。 我们将在利用函数库时提高它的操作速度(“从简单到复杂” 基础)。
现在我们来看看如何安排更新市场价订单和仓位集合清单的方法。
在方法的最开始,当前市场数据的结构,事件标志, 交易量变化值,以及所有关于订单和仓位数量的变量均被重置,集合清单也被清除。
然后检查是属于 MQL4 亦或 MQL5。
从现阶段开始,我们准备 MQL5 代码,我们来看看 MQL5 版本:
与 MQL4 不同,在 MQL5 中,订单和仓位存储在不同的列表中。
所以,获取账户内的仓位总数,然后在一个循环中遍历所有终端里的仓位,选择下一笔仓位的票据,创建一个仓位对象并将其添加到活动订单和函数库仓位的集合列表当中。
以同样的方式,添加帐户中当前存在的所有挂单。 仅获取帐户内挂单的总数,并在循环里遍历终端的订单列表接收订单票据,并添加订单对象至函数库激活订单和仓位集合列表。
两个循环完成后,函数库的激活订单和仓位集合列表将包含当前帐户中存在的订单和仓位对象。 接下来,检查首次启动标志(此处,“前次”哈希值的数值等于 -1 作为标志)。 如果这是首次启动,则利用 SavePrevValues() 方法将所有“过去”的数值复制到存储这些数值的结构中。 如果这不是第一次运行,则将过去的哈希值与计算出的当前哈希值进行比较。 如果先前的哈希值不等于当前哈希值,则帐户里发生了变化
在此情况下,当前和先前订单数量之间的差值会设置在存储新帐户订单数量的变量中,而当前和先前仓位数量之间的差值设置在存储新帐户仓位数量的变量中。 保存帐户交易量总数的变化值,设置交易量变化标志,以及发生交易事件的标志 ,最后,使用 SavePrevValues() 方法为 “previous” 值的结构添加新值,以便进行后续验证。
SavePrevValues() 方法只是将含有当前值的结构复制到含有先前值的结构中。
若要检查市价订单和仓位清单更新方法,及其与历史订单和成交清单更新方法的联合工作操作,使用 Part03 文件夹中名为 TestDoEasyPart03_4.mq5 的最后一个测试 EA:
//+------------------------------------------------------------------+ //| TestDoEasyPart03_4.mq5 | //| 版权所有 2018, MetaQuotes 软件公司 | //| https://mql5.com/zh/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "版权所有 2018, MetaQuotes 软件公司" #property link "https://mql5.com/zh/users/artmedia70" #property version "1.00" //--- 包含 #include <DoEasy\Engine.mqh> //--- 全局变量 CEngine engine; //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能系统即时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+ //| 计时器函数 | //+------------------------------------------------------------------+ void OnTimer() { engine.OnTimer(); } //+------------------------------------------------------------------+
若要查看添加和跟踪市价订单和仓位集合时实施的变化,请将以下字符串添加到 CEngine 类的 Timer 事件处理程序中:
//+------------------------------------------------------------------+ //| CEngine 计时器 | //+------------------------------------------------------------------+ void CEngine::OnTimer(void) { //--- 历史订单和成交集合定时器,以及市价订单和仓位 int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); if(counter!=NULL && counter.IsTimeDone()) { //--- 更新列表 this.m_market.Refresh(); this.m_history.Refresh(); //--- 首次启动动作 if(this.IsFirstStart()) { return; } //--- 检查市场状况变化 if(this.m_market.IsTradeEvent()) { Print(DFUN,TextByLanguage("Новое торговое событие на счёте","New trading event on account")); } //--- 检查帐户历史更改 if(this.m_history.IsTradeEvent()) { Print(DFUN,TextByLanguage("Новое торговое событие в истории счёта","New trading event in account history")); } } } } //+------------------------------------------------------------------+
这里的一切都很简单。 首先检查集合定时器计数器中的等待完成情况。 如果暂停结束,则更新市价单和仓位列表以及历史订单和成交。 首次启动时无需任何操作。 收到帐户事件发生的标志时,在流水日志中显示相应的方法。 当接收帐户历史记录中的事件标志时,也会这样做。
编译测试 EA 并启动它。 现在,如果我们开立一笔仓位,流水日志中会出现两个条目:
2019.02.28 17:36:24.678 CEngine::OnTimer: New trading event on the account 2019.02.28 17:36:24.678 CEngine::OnTimer: New trading event in the account history
第一个表示帐户上发生了交易事件,而第二个表示将新事件添加到帐户历史记录中。 在这种情况下,帐户内的交易事件包含仓位数增加 1,而帐户历史记录中的新事件则表示出现一笔新的市价开单和一笔新的成交 — “入场”。
如果我们现在将一笔持仓平仓,则相同的两个条目出现在流水日志中,但现在正读取得数值是不同的:账户内的交易事件是将仓位数量减 1,而在帐户历史记录的新事件是正在添加一笔新的市价单和一笔新的成交 — “离场”。
下一步是什么?
在下一篇文章中,我们将继续开发主体函数库元素(CEngine 类),实现处理来自集合的事件,并将它们发送到程序。
下面附有当前版本函数库的所有文件和测试 EA 文件,供您测试和下载。
在评论中留下您的问题、意见和建议。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/5687
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




不知何故,Defines.mqh 中的ORDER_STATUS_MARKET_ACTIVE 被ORDER_STATUS_MARKET_POSITION 取代了。在整个项目 中,以前写ORDER_STATUS_MARKET_ACTIVE 的 地方都应改为ORDER_STATUS_MARKET_POSITION。
这并不是什么大的说明,对于那些还将仔细检查并提升自己技能的人来说...关于这一系列文章。=)
不知何故,Defines.mqh 中的ORDER_STATUS_MARKET_ACTIVE 被ORDER_STATUS_MARKET_POSITION 取代了。在整个项目中,凡是之前写入ORDER_STATUS_MARKET_ACTIVE 的 地方,都应替换为ORDER_STATUS_MARKET_POSITION。
这并不是什么大的说明,对于那些也会仔细检查并提升自己技能的人来说...关于这一系列文章。=)
是的,有时有些东西是 "悄悄 "改动的--没有必要描述它们,但要替换它们并不困难。文章中也经常提到一些小的调整。
所有内容都在附件中,而文章描述的是本质。
伟大的作品!祝福你,繁荣昌盛。
没有批评,但应该有有益的批评。在项目工作中,我们仍然忽略了已经存在的标准功能。
交易时
在交易服务器上的交易 操作结束时产生的交易 事件 中,在 EA 中调用该函数
OnTradeTransaction
当发生TradeTransaction 事件 以处理交易请求执行结果时,在 EA 中调用该函数。
干得好!祝福你,生意兴隆。
没有批评,但应该有有益的批评。不过,在项目工作中,我们还是忽略了已经标准的功能。
交易时
在交易服务器上的交易 操作结束时产生的交易 事件 中,EA 会调用该函数
OnTradeTransaction
当发生TradeTransaction 事件 以处理交易请求执行结果时,在 EA 中调用该函数
我故意绕过了它们。
1.与 MQL4 兼容
2.避免使用时偶尔丢失事件。
我知道你想说什么,但我不是 mql 的新手。谁能帮我解决这个问题?
在 "TimerCounter.mqh "文件中,"const CTimerCounter *counter_compared = node; "在编译时出错:"'=' - 类型不匹配"。
我明白你的意思,但我是 mql 的新手。谁能帮我解决这个问题?