通过推送通知监控交易——一个MetaTrader 5服务的示例
内容
引言
当在金融市场上进行交易时,一个关键要素是能够获取关于过去某一段时间内交易结果的信息。
或许,每位交易者都至少有过一次需要监控过去一天、一周、一个月等时间段内的交易结果,以便根据这些交易结果来调整自己的交易策略的经历。MetaTrader 5客户端提供了以报告形式展现的良好统计数据,使我们能够以直观便捷的方式评估交易结果。这份报告有助于我们优化投资组合,同时也能帮助我们理解如何降低风险并提高交易的稳定性。
要分析策略,请在交易历史上下文菜单中点击“报告”,或在“查看”菜单中点击“报告”(或直接按Alt+E):
| |
在文章《MetaTrader新版报告:五大最重要交易指标》中可以找到更多详细信息。
倘若因某些缘由,客户端终端所提供的标准报告未能满足您的需求,MQL5语言则提供了丰富的可能性,让您能够创建自己的程序,包括那些用于生成报告并将其发送至交易者智能手机的程序。这就是我们今天要讨论的内容。
我们的程序应该在终端启动时开始运行,跟踪交易账户的变化、白天的开始时间以及创建和发送报告的时间。对于这种用途,服务类型的程序将非常适合我们。
根据MQL5参考手册,服务(Service)是一种程序,与指标、智能交易系统(EAs)和脚本不同,它无需连接到图表即可运行。与脚本类似,服务除了触发事件外,不处理任何其他事件。若要启动服务,其代码应包含OnStart处理程序函数。服务除了启动事件(Start)外,不接受任何其他事件,但它们能够使用EventChartCustom向图表发送自定义事件。服务存储在<terminal_directory>\MQL5\Services目录中。
每一个在终端中运行的服务都在其自身的流程中运作。这意味着一个循环运行的服务不会影响其他程序的运作。我们的服务应当在一个无限循环中工作,检查指定的时间,读取整个交易历史,创建已平仓头寸的列表,根据不同的标准对这些列表进行排序,并将报告显示在日志中,并通过推送通知发送到用户的智能手机上。此外,当服务首次启动或其设置发生更改时,服务应检查从终端发送推送通知的可能性。为了实现这一点,我们应当通过消息窗口与用户进行交互,等待用户的响应和反应。同时,在发送推送通知时,对单位时间内通知的频率存在限制。因此,有必要在发送通知时设置延迟。所有这些都不应以任何方式影响客户端终端中运行的其他应用程序的操作。基于以上所有考虑,服务是创建此类项目最方便的工具。
现在有必要形成一个将所有这些组件组装在一起的方案。
项目结构
让我们从后往前审视这个程序及其组件:
- 服务应用。该应用可以访问在服务连续运行期间所有活跃账户的数据。应用会从账户数据中接收已平仓头寸的列表,并将它们合并成一个总列表。根据设置,服务可以仅使用当前活跃账户中的已平仓头寸数据,或使用客户端终端中当前及之前使用过的每个账户的已平仓头寸数据。
基于从账户列表中获取的已平仓头寸数据,为所需的交易时段创建交易统计信息。然后,它将以推送通知的形式发送到用户的智能手机。此外,交易统计信息还会以表格形式显示在终端的专家(Experts)日志中。 - 账户集合。集合包含服务连续运行期间终端连接过的账户列表。账户集合可以访问列表中的任何账户以及所有账户的已平仓头寸。这些列表可在服务应用中查看,服务会根据它们进行选择并创建统计信息。
- 账户对象类。存储一个账户的数据,包括该账户在服务连续运行期间所有已平仓头寸的列表(集合)。提供访问账户属性的功能,可以创建和更新该账户的已平仓头寸列表,并根据各种选择标准返回已平仓头寸列表。
- 历史头寸集合类。包含头寸对象的列表,提供访问已平仓头寸属性的功能,可以创建和更新头寸列表。返回已平仓头寸列表。
- 头寸对象类。存储并提供访问已平仓头寸属性的功能。包含通过各种属性比较两个对象的功能,从而可以根据各种选择标准创建头寸列表。包含该头寸的成交订单列表,并提供访问这些成交的功能。
- 成交订单对象类。存储并提供访问单个成交订单属性的功能。对象包含通过各种属性比较两个对象的功能,从而可以根据各种选择标准创建成交订单列表。
在文章《如何在图表上直接查看成交记录而不必在交易历史中挣扎》中,我们讨论了从历史成交列表中恢复已平仓头寸的概念。成交订单列表允许通过其属性中设置的头寸ID(PositionID)来确定每笔交易与特定头寸的关联。创建一个头寸对象,将找到的成交订单放入其列表中。在这里,我们将采用同样的方式。但是,为了构建成交订单和头寸对象,我们将使用一个完全不同且经过长期测试的概念,其中每个对象都有相同的方法来设置和获取属性的功能。这个概念允许我们在一个共同的键中创建对象,将它们存储在列表中,根据任何对象属性进行过滤和排序,并根据指定的属性获取新的列表。
阅读以下文章以正确理解本项目中构建类的概念:
- 对象属性结构《(第一部分):概念、数据管理和初步结果》,
- 对象列表结构《(第二部分):历史订单和成交的集合》以及
- 根据属性过滤列表中对象的方法《(第三部分):市价订单和头寸的集合,搜索和排序》
实质上,这三篇文章描述了在MQL5中为任何对象创建数据库、将其存储在数据库中并根据所需属性和值进行获取的可能性。这正是本项目所需的功能,因此决定根据文章中描述的概念构建对象和它们的集合。但在这里会做得更简单一些——不创建具有受保护构造函数的抽象对象类,也不在类中定义不受支持的对象属性。一切都会更简单——每个对象都会有自己的属性列表,存储在三个数组中,并具有写入和检索它们的功能。所有这些对象都将存储在列表中,其中可以根据指定的属性获取仅包含所需对象的新列表。
简而言之,在项目中创建的每个对象都将拥有一组自己的属性,正如MQL5中的任何对象或实体确实如此。仅在MQL5中,有标准函数用于获取属性,而对于项目对象,这些方法将用于获取直接在每个对象的类中设置的整数、实数和字符串属性。接下来,所有这些对象都将存储在列表中——即指向CObject对象的动态数组,这些对象是标准库的一部分。标准库类允许我们以最小的成本创建复杂的项目。在这种情况下,这意味着一个包含所有交易账户已平仓头寸的数据库,且能够按任何所需属性对对象列表进行排序和筛选。
任何头寸仅在开仓(执行In deal)到平仓(执行Out/OutBuy deal)期间存在。换句话说,它是一个仅作为市场对象存在的对象。相反,任何成交仅是一个历史对象,因为成交仅仅是交易订单(交易指令)执行的事实。因此,在客户端终端的历史列表中不存在头寸——它们仅存在于当前市场头寸列表中。
因此,为了重新创建一个已经平仓的市场头寸,需要从历史成交中“组装”出之前存在的头寸。幸运的是,每笔成交订单都包含所参与头寸的ID。我们需要遍历历史成交列表,从列表中获取下一笔成交订单,检查头寸ID,并创建头寸对象。将创建的成交订单对象添加到新的历史头寸中。我们将在后续实现这一点。与此同时,让我们创建将继续使用的成交订单和头寸对象的类。
交易类
在终端的\MQL5\Services\, 目录下,新建一个名为AccountReporter\的文件夹,并在其中创建一个名为Deal.mqh的新文件,该文件将定义CDeal类。
该类应从标准库的基类CObject派生,同时其文件应被包含到新创建的类中:
//+------------------------------------------------------------------+ //| Deal.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Object.mqh> //+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { }
现在,我们来添加整数、实数和字符串类型的成交属性枚举,并在类的私有、受保护和公共区域中声明用于处理这些成交属性的成员变量和方法。
//+------------------------------------------------------------------+ //| Deal.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Object.mqh> //--- Enumeration of integer deal properties enum ENUM_DEAL_PROPERTY_INT { DEAL_PROP_TICKET = 0, // Deal ticket DEAL_PROP_ORDER, // Deal order number DEAL_PROP_TIME, // Deal execution time DEAL_PROP_TIME_MSC, // Deal execution time in milliseconds DEAL_PROP_TYPE, // Deal type DEAL_PROP_ENTRY, // Deal direction DEAL_PROP_MAGIC, // Deal magic number DEAL_PROP_REASON, // Deal execution reason or source DEAL_PROP_POSITION_ID, // Position ID DEAL_PROP_SPREAD, // Spread when performing a deal }; //--- Enumeration of real deal properties enum ENUM_DEAL_PROPERTY_DBL { DEAL_PROP_VOLUME = DEAL_PROP_SPREAD+1,// Deal volume DEAL_PROP_PRICE, // Deal price DEAL_PROP_COMMISSION, // Commission DEAL_PROP_SWAP, // Accumulated swap when closing DEAL_PROP_PROFIT, // Deal financial result DEAL_PROP_FEE, // Deal fee DEAL_PROP_SL, // Stop Loss level DEAL_PROP_TP, // Take Profit level }; //--- Enumeration of string deal properties enum ENUM_DEAL_PROPERTY_STR { DEAL_PROP_SYMBOL = DEAL_PROP_TP+1, // Symbol the deal is executed for DEAL_PROP_COMMENT, // Deal comment DEAL_PROP_EXTERNAL_ID, // Deal ID in an external trading system }; //+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { private: MqlTick m_tick; // Deal tick structure long m_lprop[DEAL_PROP_SPREAD+1]; // Array for storing integer properties double m_dprop[DEAL_PROP_TP-DEAL_PROP_SPREAD]; // Array for storing real properties string m_sprop[DEAL_PROP_EXTERNAL_ID-DEAL_PROP_TP]; // Array for storing string properties //--- Return the index of the array the deal's (1) double and (2) string properties are located at int IndexProp(ENUM_DEAL_PROPERTY_DBL property) const { return(int)property-DEAL_PROP_SPREAD-1; } int IndexProp(ENUM_DEAL_PROPERTY_STR property) const { return(int)property-DEAL_PROP_TP-1; } //--- Get a (1) deal tick and (2) a spread of the deal minute bar bool GetDealTick(const int amount=20); int GetSpreadM1(void); //--- Return time with milliseconds string TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const; protected: //--- Additional properties int m_digits; // Symbol Digits double m_point; // Symbol Point double m_bid; // Bid when performing a deal double m_ask; // Ask when performing a deal public: //--- Set the properties //--- Set deal's (1) integer, (2) real and (3) string properties void SetProperty(ENUM_DEAL_PROPERTY_INT property,long value){ this.m_lprop[property]=value; } void SetProperty(ENUM_DEAL_PROPERTY_DBL property,double value){ this.m_dprop[this.IndexProp(property)]=value; } void SetProperty(ENUM_DEAL_PROPERTY_STR property,string value){ this.m_sprop[this.IndexProp(property)]=value; } //--- Integer properties void SetTicket(const long ticket) { this.SetProperty(DEAL_PROP_TICKET, ticket); } // Ticket void SetOrder(const long order) { this.SetProperty(DEAL_PROP_ORDER, order); } // Order void SetTime(const datetime time) { this.SetProperty(DEAL_PROP_TIME, time); } // Time void SetTimeMsc(const long value) { this.SetProperty(DEAL_PROP_TIME_MSC, value); } // Time in milliseconds void SetTypeDeal(const ENUM_DEAL_TYPE type) { this.SetProperty(DEAL_PROP_TYPE, type); } // Type void SetEntry(const ENUM_DEAL_ENTRY entry) { this.SetProperty(DEAL_PROP_ENTRY, entry); } // Direction void SetMagic(const long magic) { this.SetProperty(DEAL_PROP_MAGIC, magic); } // Magic number void SetReason(const ENUM_DEAL_REASON reason) { this.SetProperty(DEAL_PROP_REASON, reason); } // Deal execution reason or source void SetPositionID(const long id) { this.SetProperty(DEAL_PROP_POSITION_ID, id); } // Position ID //--- Real properties void SetVolume(const double volume) { this.SetProperty(DEAL_PROP_VOLUME, volume); } // Volume void SetPrice(const double price) { this.SetProperty(DEAL_PROP_PRICE, price); } // Price void SetCommission(const double value) { this.SetProperty(DEAL_PROP_COMMISSION, value); } // Commission void SetSwap(const double value) { this.SetProperty(DEAL_PROP_SWAP, value); } // Accumulated swap when closing void SetProfit(const double value) { this.SetProperty(DEAL_PROP_PROFIT, value); } // Financial result void SetFee(const double value) { this.SetProperty(DEAL_PROP_FEE, value); } // Deal fee void SetSL(const double value) { this.SetProperty(DEAL_PROP_SL, value); } // Stop Loss level void SetTP(const double value) { this.SetProperty(DEAL_PROP_TP, value); } // Take Profit level //--- String properties void SetSymbol(const string symbol) { this.SetProperty(DEAL_PROP_SYMBOL,symbol); } // Symbol name void SetComment(const string comment) { this.SetProperty(DEAL_PROP_COMMENT,comment); } // Comment void SetExternalID(const string ext_id) { this.SetProperty(DEAL_PROP_EXTERNAL_ID,ext_id); } // Deal ID in an external trading system //--- Get the properties //--- Return deal’s (1) integer, (2) real and (3) string property from the properties array long GetProperty(ENUM_DEAL_PROPERTY_INT property) const { return this.m_lprop[property]; } double GetProperty(ENUM_DEAL_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)]; } string GetProperty(ENUM_DEAL_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)]; } //--- Integer properties long Ticket(void) const { return this.GetProperty(DEAL_PROP_TICKET); } // Ticket long Order(void) const { return this.GetProperty(DEAL_PROP_ORDER); } // Order datetime Time(void) const { return (datetime)this.GetProperty(DEAL_PROP_TIME); } // Time long TimeMsc(void) const { return this.GetProperty(DEAL_PROP_TIME_MSC); } // Time in milliseconds ENUM_DEAL_TYPE TypeDeal(void) const { return (ENUM_DEAL_TYPE)this.GetProperty(DEAL_PROP_TYPE); } // Type ENUM_DEAL_ENTRY Entry(void) const { return (ENUM_DEAL_ENTRY)this.GetProperty(DEAL_PROP_ENTRY); } // Direction long Magic(void) const { return this.GetProperty(DEAL_PROP_MAGIC); } // Magic number ENUM_DEAL_REASON Reason(void) const { return (ENUM_DEAL_REASON)this.GetProperty(DEAL_PROP_REASON); } // Deal execution reason or source long PositionID(void) const { return this.GetProperty(DEAL_PROP_POSITION_ID); } // Position ID //--- Real properties double Volume(void) const { return this.GetProperty(DEAL_PROP_VOLUME); } // Volume double Price(void) const { return this.GetProperty(DEAL_PROP_PRICE); } // Price double Commission(void) const { return this.GetProperty(DEAL_PROP_COMMISSION); } // Commission double Swap(void) const { return this.GetProperty(DEAL_PROP_SWAP); } // Accumulated swap when closing double Profit(void) const { return this.GetProperty(DEAL_PROP_PROFIT); } // Financial result double Fee(void) const { return this.GetProperty(DEAL_PROP_FEE); } // Deal fee double SL(void) const { return this.GetProperty(DEAL_PROP_SL); } // Stop Loss level double TP(void) const { return this.GetProperty(DEAL_PROP_TP); } // Take Profit level //--- String properties string Symbol(void) const { return this.GetProperty(DEAL_PROP_SYMBOL); } // Symbol name string Comment(void) const { return this.GetProperty(DEAL_PROP_COMMENT); } // Comment string ExternalID(void) const { return this.GetProperty(DEAL_PROP_EXTERNAL_ID); } // Deal ID in an external trading system //--- Additional properties double Bid(void) const { return this.m_bid; } // Bid when performing a deal double Ask(void) const { return this.m_ask; } // Ask when performing a deal int Spread(void) const { return (int)this.GetProperty(DEAL_PROP_SPREAD); } // Spread when performing a deal //--- Return the description of a (1) deal type, (2) position change method and (3) deal reason string TypeDescription(void) const; string EntryDescription(void) const; string ReasonDescription(void) const; //--- Return deal description string Description(void); //--- Print deal properties in the journal void Print(void); //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const; //--- Constructors/destructor CDeal(void){} CDeal(const ulong ticket); ~CDeal(); };
我们来看一下类方法的实现。
在类构造函数中,我们假设成交订单已经被选中,因此我们可以获取它的属性:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CDeal::CDeal(const ulong ticket) { //--- Store the properties //--- Integer properties this.SetTicket((long)ticket); // Deal ticket this.SetOrder(::HistoryDealGetInteger(ticket, DEAL_ORDER)); // Order this.SetTime((datetime)::HistoryDealGetInteger(ticket, DEAL_TIME)); // Deal execution time this.SetTimeMsc(::HistoryDealGetInteger(ticket, DEAL_TIME_MSC)); // Deal execution time in milliseconds this.SetTypeDeal((ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE)); // Type this.SetEntry((ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY)); // Direction this.SetMagic(::HistoryDealGetInteger(ticket, DEAL_MAGIC)); // Magic number this.SetReason((ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON)); // Deal execution reason or source this.SetPositionID(::HistoryDealGetInteger(ticket, DEAL_POSITION_ID)); // Position ID //--- Real properties this.SetVolume(::HistoryDealGetDouble(ticket, DEAL_VOLUME)); // Volume this.SetPrice(::HistoryDealGetDouble(ticket, DEAL_PRICE)); // Price this.SetCommission(::HistoryDealGetDouble(ticket, DEAL_COMMISSION)); // Commission this.SetSwap(::HistoryDealGetDouble(ticket, DEAL_SWAP)); // Accumulated swap when closing this.SetProfit(::HistoryDealGetDouble(ticket, DEAL_PROFIT)); // Financial result this.SetFee(::HistoryDealGetDouble(ticket, DEAL_FEE)); // Deal fee this.SetSL(::HistoryDealGetDouble(ticket, DEAL_SL)); // Stop Loss level this.SetTP(::HistoryDealGetDouble(ticket, DEAL_TP)); // Take Profit level //--- String properties this.SetSymbol(::HistoryDealGetString(ticket, DEAL_SYMBOL)); // Symbol name this.SetComment(::HistoryDealGetString(ticket, DEAL_COMMENT)); // Comment this.SetExternalID(::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID)); // Deal ID in an external trading system //--- Additional parameters this.m_digits = (int)::SymbolInfoInteger(this.Symbol(), SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.Symbol(), SYMBOL_POINT); //--- Parameters for calculating spread this.m_bid = 0; this.m_ask = 0; this.SetProperty(DEAL_PROP_SPREAD, 0); //--- If the historical tick and the Point value of the symbol were obtained if(this.GetDealTick() && this.m_point!=0) { //--- set the Bid and Ask price values, calculate and save the spread value this.m_bid=this.m_tick.bid; this.m_ask=this.m_tick.ask; int spread=(int)::fabs((this.m_ask-this.m_bid)/this.m_point); this.SetProperty(DEAL_PROP_SPREAD, spread); } //--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on else this.SetProperty(DEAL_PROP_SPREAD, this.GetSpreadM1()); }
将成交属性,以及成交所用交易品种的精度(Digits)和点值(Point)保存在类的属性数组中,以便进行计算并显示成交信息。接下来,获取成交时的历史价格数据。这样,我们就可以获取成交时的买价和卖价,从而计算出点差(spread)。
通过指定属性比较两个对象的方法:
//+------------------------------------------------------------------+ //| Compare two objects by the specified property | //+------------------------------------------------------------------+ int CDeal::Compare(const CObject *node,const int mode=0) const { const CDeal * obj = node; switch(mode) { case DEAL_PROP_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case DEAL_PROP_ORDER : return(this.Order() > obj.Order() ? 1 : this.Order() < obj.Order() ? -1 : 0); case DEAL_PROP_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case DEAL_PROP_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case DEAL_PROP_TYPE : return(this.TypeDeal() > obj.TypeDeal() ? 1 : this.TypeDeal() < obj.TypeDeal() ? -1 : 0); case DEAL_PROP_ENTRY : return(this.Entry() > obj.Entry() ? 1 : this.Entry() < obj.Entry() ? -1 : 0); case DEAL_PROP_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case DEAL_PROP_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case DEAL_PROP_POSITION_ID : return(this.PositionID() > obj.PositionID() ? 1 : this.PositionID() < obj.PositionID() ? -1 : 0); case DEAL_PROP_SPREAD : return(this.Spread() > obj.Spread() ? 1 : this.Spread() < obj.Spread() ? -1 : 0); case DEAL_PROP_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case DEAL_PROP_PRICE : return(this.Price() > obj.Price() ? 1 : this.Price() < obj.Price() ? -1 : 0); case DEAL_PROP_COMMISSION : return(this.Commission() > obj.Commission() ? 1 : this.Commission() < obj.Commission() ? -1 : 0); case DEAL_PROP_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case DEAL_PROP_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case DEAL_PROP_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case DEAL_PROP_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case DEAL_PROP_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case DEAL_PROP_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case DEAL_PROP_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case DEAL_PROP_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); default : return(-1); } }
这是一个虚方法,它重写了CObject父类中同名的方法。根据比较模式(成交对象的一个属性),对当前对象的这些属性和通过指针传递给方法的另一个对象的属性进行比较。如果当前对象属性的值大于比较对象的值,该方法返回1。如果小于,则返回-1。如果两者值相等,则返回0。
返回交易类型描述的方法:
//+------------------------------------------------------------------+ //| Return the deal type description | //+------------------------------------------------------------------+ string CDeal::TypeDescription(void) const { switch(this.TypeDeal()) { case DEAL_TYPE_BUY : return "Buy"; case DEAL_TYPE_SELL : return "Sell"; case DEAL_TYPE_BALANCE : return "Balance"; case DEAL_TYPE_CREDIT : return "Credit"; case DEAL_TYPE_CHARGE : return "Additional charge"; case DEAL_TYPE_CORRECTION : return "Correction"; case DEAL_TYPE_BONUS : return "Bonus"; case DEAL_TYPE_COMMISSION : return "Additional commission"; case DEAL_TYPE_COMMISSION_DAILY : return "Daily commission"; case DEAL_TYPE_COMMISSION_MONTHLY : return "Monthly commission"; case DEAL_TYPE_COMMISSION_AGENT_DAILY : return "Daily agent commission"; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY: return "Monthly agent commission"; case DEAL_TYPE_INTEREST : return "Interest rate"; case DEAL_TYPE_BUY_CANCELED : return "Canceled buy deal"; case DEAL_TYPE_SELL_CANCELED : return "Canceled sell deal"; case DEAL_DIVIDEND : return "Dividend operations"; case DEAL_DIVIDEND_FRANKED : return "Franked (non-taxable) dividend operations"; case DEAL_TAX : return "Tax charges"; default : return "Unknown: "+(string)this.TypeDeal(); } }
根据成交类型,返回其文本描述。对于这个项目来说,这个方法有些多余,因为我们不会使用所有类型的成交订单,而只关注与仓位相关的成交类型——即买入或卖出。
返回头寸变更方法描述的方法:
//+------------------------------------------------------------------+ //| Return position change method | //+------------------------------------------------------------------+ string CDeal::EntryDescription(void) const { switch(this.Entry()) { case DEAL_ENTRY_IN : return "Entry In"; case DEAL_ENTRY_OUT : return "Entry Out"; case DEAL_ENTRY_INOUT : return "Reverse"; case DEAL_ENTRY_OUT_BY : return "Close a position by an opposite one"; default : return "Unknown: "+(string)this.Entry(); } }
返回交易原因描述的方法:
//+------------------------------------------------------------------+ //| Return a deal reason description | //+------------------------------------------------------------------+ string CDeal::ReasonDescription(void) const { switch(this.Reason()) { case DEAL_REASON_CLIENT : return "Terminal"; case DEAL_REASON_MOBILE : return "Mobile"; case DEAL_REASON_WEB : return "Web"; case DEAL_REASON_EXPERT : return "EA"; case DEAL_REASON_SL : return "SL"; case DEAL_REASON_TP : return "TP"; case DEAL_REASON_SO : return "SO"; case DEAL_REASON_ROLLOVER : return "Rollover"; case DEAL_REASON_VMARGIN : return "Var. Margin"; case DEAL_REASON_SPLIT : return "Split"; case DEAL_REASON_CORPORATE_ACTION: return "Corp. Action"; default : return "Unknown reason "+(string)this.Reason(); } }
返回交易描述的方法:
//+------------------------------------------------------------------+ //| Return deal description | //+------------------------------------------------------------------+ string CDeal::Description(void) { return(::StringFormat("Deal: %-9s %.2f %-4s #%I64d at %s", this.EntryDescription(), this.Volume(), this.TypeDescription(), this.Ticket(), this.TimeMscToString(this.TimeMsc()))); }
在日志中打印交易属性的方法:
//+------------------------------------------------------------------+ //| Print deal properties in the journal | //+------------------------------------------------------------------+ void CDeal::Print(void) { ::Print(this.Description()); }
返回带毫秒的时间的方法:
//+------------------------------------------------------------------+ //| Return time with milliseconds | //+------------------------------------------------------------------+ string CDeal::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0')); }
所有返回和记录文本描述的方法都是为了描述成交。在这个项目中,它们实际上并不是必需的,但人们应该时刻记得可能会有扩展和改进的需求。出于这个原因,这里包含了这样的方法。
接收成交价格数据的方法:
//+------------------------------------------------------------------+ //| Get the deal tick | //| https://www.mql5.com/ru/forum/42122/page47#comment_37205238 | //+------------------------------------------------------------------+ bool CDeal::GetDealTick(const int amount=20) { MqlTick ticks[]; // We will receive ticks here int attempts = amount; // Number of attempts to get ticks int offset = 500; // Initial time offset for an attempt int copied = 0; // Number of ticks copied //--- Until the tick is copied and the number of copy attempts is over //--- we try to get a tick, doubling the initial time offset at each iteration (expand the "from_msc" time range) while(!::IsStopped() && (copied<=0) && (attempts--)!=0) copied = ::CopyTicksRange(this.Symbol(), ticks, COPY_TICKS_INFO, this.TimeMsc()-(offset <<=1), this.TimeMsc()); //--- If the tick was successfully copied (it is the last one in the tick array), set it to the m_tick variable if(copied>0) this.m_tick=ticks[copied-1]; //--- Return the flag that the tick was copied return(copied>0); }
方法的逻辑已在代码注释中讲述。在接收到一个价格变动(tick)后,会从其中获取卖出价和买入价,并计算点差大小,公式为(卖出价 - 买入价)/ 点值(Point)。
如果使用此方法无法获取滴点数据,则使用获取成交分钟柱形图点差的方法来获取点差的平均值:
//+------------------------------------------------------------------+ //| Gets the spread of the deal minute bar | //+------------------------------------------------------------------+ int CDeal::GetSpreadM1(void) { int array[1]={}; int bar=::iBarShift(this.Symbol(), PERIOD_M1, this.Time()); if(bar==WRONG_VALUE) return 0; return(::CopySpread(this.Symbol(), PERIOD_M1, bar, 1, array)==1 ? array[0] : 0); }
成交类已经准备就绪。类对象将被存储在历史仓位类的成交列表中,从该列表中可以获取所需成交的指针并处理其数据。
历史仓位类
在\MQL5\ServicesAccountReporter中,创建新的Position.mqh文件,用于定义CPosition类。
该类应从标准库的CObject基类继承:
//+------------------------------------------------------------------+ //| Position.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { }
由于仓位类将包含该仓位的列表,因此需要在所创建的文件中包含成交类文件以及动态数组类文件,该数组类用于存储指向CObject对象的指针:
//+------------------------------------------------------------------+ //| Position.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Deal.mqh" #include <Arrays\ArrayObj.mqh> //+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { }
现在,让我们添加整数、实数和字符串类型的交易属性枚举,并在私有、受保护和公共部分中声明处理仓位属性的类成员变量和方法:
//+------------------------------------------------------------------+ //| Position.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Deal.mqh" #include <Arrays\ArrayObj.mqh> //--- Enumeration of integer position properties enum ENUM_POSITION_PROPERTY_INT { POSITION_PROP_TICKET = 0, // Position ticket POSITION_PROP_TIME, // Position open time POSITION_PROP_TIME_MSC, // Position open time in milliseconds POSITION_PROP_TIME_UPDATE, // Position change time POSITION_PROP_TIME_UPDATE_MSC, // Position change time in milliseconds POSITION_PROP_TYPE, // Position type POSITION_PROP_MAGIC, // Position magic number POSITION_PROP_IDENTIFIER, // Position ID POSITION_PROP_REASON, // Position open reason POSITION_PROP_ACCOUNT_LOGIN, // Account number POSITION_PROP_TIME_CLOSE, // Position close time POSITION_PROP_TIME_CLOSE_MSC, // Position close time in milliseconds }; //--- Enumeration of real position properties enum ENUM_POSITION_PROPERTY_DBL { POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+1,// Position volume POSITION_PROP_PRICE_OPEN, // Position price POSITION_PROP_SL, // Stop Loss for open position POSITION_PROP_TP, // Take Profit for open position POSITION_PROP_PRICE_CURRENT, // Symbol current price POSITION_PROP_SWAP, // Accumulated swap POSITION_PROP_PROFIT, // Current profit POSITION_PROP_CONTRACT_SIZE, // Symbol trade contract size POSITION_PROP_PRICE_CLOSE, // Position close price POSITION_PROP_COMMISSIONS, // Accumulated commission POSITION_PROP_FEE, // Accumulated payment for deals }; //--- Enumeration of string position properties enum ENUM_POSITION_PROPERTY_STR { POSITION_PROP_SYMBOL = POSITION_PROP_FEE+1,// A symbol the position is open for POSITION_PROP_COMMENT, // Comment to a position POSITION_PROP_EXTERNAL_ID, // Position ID in the external system POSITION_PROP_CURRENCY_PROFIT, // Position symbol profit currency POSITION_PROP_ACCOUNT_CURRENCY, // Account deposit currency POSITION_PROP_ACCOUNT_SERVER, // Server name }; //+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { private: long m_lprop[POSITION_PROP_TIME_CLOSE_MSC+1]; // Array for storing integer properties double m_dprop[POSITION_PROP_FEE-POSITION_PROP_TIME_CLOSE_MSC]; // Array for storing real properties string m_sprop[POSITION_PROP_ACCOUNT_SERVER-POSITION_PROP_FEE]; // Array for storing string properties //--- Return the index of the array the order's (1) double and (2) string properties are located at int IndexProp(ENUM_POSITION_PROPERTY_DBL property) const { return(int)property-POSITION_PROP_TIME_CLOSE_MSC-1;} int IndexProp(ENUM_POSITION_PROPERTY_STR property) const { return(int)property-POSITION_PROP_FEE-1; } protected: CArrayObj m_list_deals; // List of position deals CDeal m_temp_deal; // Temporary deal object for searching by property in the list //--- Return time with milliseconds string TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const; //--- Additional properties int m_profit_pt; // Profit in points int m_digits; // Symbol digits double m_point; // One symbol point value double m_tick_value; // Calculated tick value //--- Return the pointer to (1) open and (2) close deal CDeal *GetDealIn(void) const; CDeal *GetDealOut(void) const; public: //--- Return the list of deals CArrayObj *GetListDeals(void) { return(&this.m_list_deals); } //--- Set the properties //--- Set (1) integer, (2) real and (3) string properties void SetProperty(ENUM_POSITION_PROPERTY_INT property,long value) { this.m_lprop[property]=value; } void SetProperty(ENUM_POSITION_PROPERTY_DBL property,double value) { this.m_dprop[this.IndexProp(property)]=value; } void SetProperty(ENUM_POSITION_PROPERTY_STR property,string value) { this.m_sprop[this.IndexProp(property)]=value; } //--- Integer properties void SetTicket(const long ticket) { this.SetProperty(POSITION_PROP_TICKET, ticket); } // Position ticket void SetTime(const datetime time) { this.SetProperty(POSITION_PROP_TIME, time); } // Position open time void SetTimeMsc(const long value) { this.SetProperty(POSITION_PROP_TIME_MSC, value); } // Position open time in milliseconds since 01.01.1970 void SetTimeUpdate(const datetime time) { this.SetProperty(POSITION_PROP_TIME_UPDATE, time); } // Position update time void SetTimeUpdateMsc(const long value) { this.SetProperty(POSITION_PROP_TIME_UPDATE_MSC, value); } // Position update time in milliseconds since 01.01.1970 void SetTypePosition(const ENUM_POSITION_TYPE type) { this.SetProperty(POSITION_PROP_TYPE, type); } // Position type void SetMagic(const long magic) { this.SetProperty(POSITION_PROP_MAGIC, magic); } // Magic number for a position (see ORDER_MAGIC) void SetID(const long id) { this.SetProperty(POSITION_PROP_IDENTIFIER, id); } // Position ID void SetReason(const ENUM_POSITION_REASON reason) { this.SetProperty(POSITION_PROP_REASON, reason); } // Position open reason void SetTimeClose(const datetime time) { this.SetProperty(POSITION_PROP_TIME_CLOSE, time); } // Close time void SetTimeCloseMsc(const long value) { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value); } // Close time in milliseconds void SetAccountLogin(const long login) { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login); } // Acount number //--- Real properties void SetVolume(const double volume) { this.SetProperty(POSITION_PROP_VOLUME, volume); } // Position volume void SetPriceOpen(const double price) { this.SetProperty(POSITION_PROP_PRICE_OPEN, price); } // Position price void SetSL(const double value) { this.SetProperty(POSITION_PROP_SL, value); } // Stop Loss level for an open position void SetTP(const double value) { this.SetProperty(POSITION_PROP_TP, value); } // Take Profit level for an open position void SetPriceCurrent(const double price) { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price); } // Current price by symbol void SetSwap(const double value) { this.SetProperty(POSITION_PROP_SWAP, value); } // Accumulated swap void SetProfit(const double value) { this.SetProperty(POSITION_PROP_PROFIT, value); } // Current profit void SetPriceClose(const double price) { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price); } // Close price void SetContractSize(const double value) { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value); } // Symbol trading contract size void SetCommissions(void); // Total commission of all deals void SetFee(void); // Total deal fee //--- String properties void SetSymbol(const string symbol) { this.SetProperty(POSITION_PROP_SYMBOL, symbol); } // Symbol a position is opened for void SetComment(const string comment) { this.SetProperty(POSITION_PROP_COMMENT, comment); } // Position comment void SetExternalID(const string ext_id) { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id); } // Position ID in an external system (on the exchange) void SetAccountServer(const string server) { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server); } // Server name void SetAccountCurrency(const string currency) { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency); } // Account deposit currency void SetCurrencyProfit(const string currency) { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency); } // Profit currency of the position symbol //--- Get the properties //--- Return (1) integer, (2) real and (3) string property from the properties array long GetProperty(ENUM_POSITION_PROPERTY_INT property) const { return this.m_lprop[property]; } double GetProperty(ENUM_POSITION_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)]; } string GetProperty(ENUM_POSITION_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)]; } //--- Integer properties long Ticket(void) const { return this.GetProperty(POSITION_PROP_TICKET); } // Position ticket datetime Time(void) const { return (datetime)this.GetProperty(POSITION_PROP_TIME); } // Position open time long TimeMsc(void) const { return this.GetProperty(POSITION_PROP_TIME_MSC); } // Position open time in milliseconds since 01.01.1970 datetime TimeUpdate(void) const { return (datetime)this.GetProperty(POSITION_PROP_TIME_UPDATE);} // Position change time long TimeUpdateMsc(void) const { return this.GetProperty(POSITION_PROP_TIME_UPDATE_MSC); } // Position update time in milliseconds since 01.01.1970 ENUM_POSITION_TYPE TypePosition(void) const { return (ENUM_POSITION_TYPE)this.GetProperty(POSITION_PROP_TYPE);}// Position type long Magic(void) const { return this.GetProperty(POSITION_PROP_MAGIC); } // Magic number for a position (see ORDER_MAGIC) long ID(void) const { return this.GetProperty(POSITION_PROP_IDENTIFIER); } // Position ID ENUM_POSITION_REASON Reason(void) const { return (ENUM_POSITION_REASON)this.GetProperty(POSITION_PROP_REASON);}// Position opening reason datetime TimeClose(void) const { return (datetime)this.GetProperty(POSITION_PROP_TIME_CLOSE); } // Close time long TimeCloseMsc(void) const { return this.GetProperty(POSITION_PROP_TIME_CLOSE_MSC); } // Close time in milliseconds long AccountLogin(void) const { return this.GetProperty(POSITION_PROP_ACCOUNT_LOGIN); } // Login //--- Real properties double Volume(void) const { return this.GetProperty(POSITION_PROP_VOLUME); } // Position volume double PriceOpen(void) const { return this.GetProperty(POSITION_PROP_PRICE_OPEN); } // Position price double SL(void) const { return this.GetProperty(POSITION_PROP_SL); } // Stop Loss level for an open position double TP(void) const { return this.GetProperty(POSITION_PROP_TP); } // Take Profit level for an open position double PriceCurrent(void) const { return this.GetProperty(POSITION_PROP_PRICE_CURRENT); } // Current price by symbol double Swap(void) const { return this.GetProperty(POSITION_PROP_SWAP); } // Accumulated swap double Profit(void) const { return this.GetProperty(POSITION_PROP_PROFIT); } // Current profit double ContractSize(void) const { return this.GetProperty(POSITION_PROP_CONTRACT_SIZE); } // Symbol trading contract size double PriceClose(void) const { return this.GetProperty(POSITION_PROP_PRICE_CLOSE); } // Close price double Commissions(void) const { return this.GetProperty(POSITION_PROP_COMMISSIONS); } // Total commission of all deals double Fee(void) const { return this.GetProperty(POSITION_PROP_FEE); } // Total deal fee //--- String properties string Symbol(void) const { return this.GetProperty(POSITION_PROP_SYMBOL); } // A symbol position is opened on string Comment(void) const { return this.GetProperty(POSITION_PROP_COMMENT); } // Position comment string ExternalID(void) const { return this.GetProperty(POSITION_PROP_EXTERNAL_ID); } // Position ID in an external system (on the exchange) string AccountServer(void) const { return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER); } // Server name string AccountCurrency(void) const { return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY); } // Account deposit currency string CurrencyProfit(void) const { return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT); } // Profit currency of the position symbol //--- Additional properties ulong DealIn(void) const; // Open deal ticket ulong DealOut(void) const; // Close deal ticket int ProfitInPoints(void) const; // Profit in points int SpreadIn(void) const; // Spread when opening int SpreadOut(void) const; // Spread when closing double SpreadOutCost(void) const; // Spread cost when closing double PriceOutAsk(void) const; // Ask price when closing double PriceOutBid(void) const; // Bid price when closing //--- Add a deal to the list of deals, return the pointer CDeal *DealAdd(const long ticket); //--- Return a position type description string TypeDescription(void) const; //--- Return position open time and price description string TimePriceCloseDescription(void); //--- Return position close time and price description string TimePriceOpenDescription(void); //--- Return position description string Description(void); //--- Print the properties of the position and its deals in the journal void Print(void); //--- Compare two objects by the property specified in 'mode' virtual int Compare(const CObject *node, const int mode=0) const; //--- Constructor/destructor CPosition(const long position_id, const string symbol); CPosition(void){} ~CPosition(); };
我们来看一下类方法的实现。
在类构造函数中,从传递给方法的参数中设置仓位ID和交易品种,并写入账户和交易品种数据:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPosition::CPosition(const long position_id, const string symbol) { this.m_list_deals.Sort(DEAL_PROP_TIME_MSC); this.SetID(position_id); this.SetSymbol(symbol); this.SetAccountLogin(::AccountInfoInteger(ACCOUNT_LOGIN)); this.SetAccountServer(::AccountInfoString(ACCOUNT_SERVER)); this.SetAccountCurrency(::AccountInfoString(ACCOUNT_CURRENCY)); this.SetCurrencyProfit(::SymbolInfoString(this.Symbol(),SYMBOL_CURRENCY_PROFIT)); this.SetContractSize(::SymbolInfoDouble(this.Symbol(),SYMBOL_TRADE_CONTRACT_SIZE)); this.m_digits = (int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.Symbol(),SYMBOL_POINT); this.m_tick_value = ::SymbolInfoDouble(this.Symbol(), SYMBOL_TRADE_TICK_VALUE); }
在类的析构函数中,清空仓位成交列表:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPosition::~CPosition() { this.m_list_deals.Clear(); }
通过指定属性比较两个对象的方法:
//+------------------------------------------------------------------+ //| Compare two objects by the specified property | //+------------------------------------------------------------------+ int CPosition::Compare(const CObject *node,const int mode=0) const { const CPosition *obj=node; switch(mode) { case POSITION_PROP_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case POSITION_PROP_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case POSITION_PROP_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case POSITION_PROP_TIME_UPDATE : return(this.TimeUpdate() > obj.TimeUpdate() ? 1 : this.TimeUpdate() < obj.TimeUpdate() ? -1 : 0); case POSITION_PROP_TIME_UPDATE_MSC : return(this.TimeUpdateMsc() > obj.TimeUpdateMsc() ? 1 : this.TimeUpdateMsc() < obj.TimeUpdateMsc() ? -1 : 0); case POSITION_PROP_TYPE : return(this.TypePosition() > obj.TypePosition() ? 1 : this.TypePosition() < obj.TypePosition() ? -1 : 0); case POSITION_PROP_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case POSITION_PROP_IDENTIFIER : return(this.ID() > obj.ID() ? 1 : this.ID() < obj.ID() ? -1 : 0); case POSITION_PROP_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case POSITION_PROP_ACCOUNT_LOGIN : return(this.AccountLogin() > obj.AccountLogin() ? 1 : this.AccountLogin() < obj.AccountLogin() ? -1 : 0); case POSITION_PROP_TIME_CLOSE : return(this.TimeClose() > obj.TimeClose() ? 1 : this.TimeClose() < obj.TimeClose() ? -1 : 0); case POSITION_PROP_TIME_CLOSE_MSC : return(this.TimeCloseMsc() > obj.TimeCloseMsc() ? 1 : this.TimeCloseMsc() < obj.TimeCloseMsc() ? -1 : 0); case POSITION_PROP_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case POSITION_PROP_PRICE_OPEN : return(this.PriceOpen() > obj.PriceOpen() ? 1 : this.PriceOpen() < obj.PriceOpen() ? -1 : 0); case POSITION_PROP_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case POSITION_PROP_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case POSITION_PROP_PRICE_CURRENT : return(this.PriceCurrent() > obj.PriceCurrent() ? 1 : this.PriceCurrent() < obj.PriceCurrent() ? -1 : 0); case POSITION_PROP_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case POSITION_PROP_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case POSITION_PROP_CONTRACT_SIZE : return(this.ContractSize() > obj.ContractSize() ? 1 : this.ContractSize() < obj.ContractSize() ? -1 : 0); case POSITION_PROP_PRICE_CLOSE : return(this.PriceClose() > obj.PriceClose() ? 1 : this.PriceClose() < obj.PriceClose() ? -1 : 0); case POSITION_PROP_COMMISSIONS : return(this.Commissions() > obj.Commissions() ? 1 : this.Commissions() < obj.Commissions() ? -1 : 0); case POSITION_PROP_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case POSITION_PROP_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case POSITION_PROP_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case POSITION_PROP_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); case POSITION_PROP_CURRENCY_PROFIT : return(this.CurrencyProfit() > obj.CurrencyProfit() ? 1 : this.CurrencyProfit() < obj.CurrencyProfit() ? -1 : 0); case POSITION_PROP_ACCOUNT_CURRENCY : return(this.AccountCurrency() > obj.AccountCurrency() ? 1 : this.AccountCurrency() < obj.AccountCurrency() ? -1 : 0); case POSITION_PROP_ACCOUNT_SERVER : return(this.AccountServer() > obj.AccountServer() ? 1 : this.AccountServer() < obj.AccountServer() ? -1 : 0); default : return -1; } }
这是一个虚方法,它重写了CObject父类中同名的方法。根据比较模式(仓位对象的一个属性),对当前对象的这些属性和通过指针传递给方法的另一个对象的属性进行比较。如果当前对象属性的值大于比较对象的值,该方法返回1。如果小于,则返回-1。如果两者值相等,则返回0。
返回带毫秒的时间的方法:
//+------------------------------------------------------------------+ //| Return time with milliseconds | //+------------------------------------------------------------------+ string CPosition::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const { return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0')); }
返回开仓交易指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the opening deal | //+------------------------------------------------------------------+ CDeal *CPosition::GetDealIn(void) const { int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; if(deal.Entry()==DEAL_ENTRY_IN) return deal; } return NULL; }
在仓位成交列表中循环,查找使用DEAL_ENTRY_IN(市价入场)仓位变更方法的交易订单,并返回其指针。
返回平仓交易指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the close deal | //+------------------------------------------------------------------+ CDeal *CPosition::GetDealOut(void) const { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) return deal; } return NULL; }
在仓位成交列表中循环,查找使用DEAL_ENTRY_OUT(市价出场)或DEAL_ENTRY_OUT_BY(平仓)仓位变更方法的成交,并返回找到的成交的指针。
返回开仓交易订单编号的方法:
//+------------------------------------------------------------------+ //| Return the open deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealIn(void) const { CDeal *deal=this.GetDealIn(); return(deal!=NULL ? deal.Ticket() : 0); }
获取市价入场成交的指针,并返回其订单编号。
返回平仓订单编号的方法:
//+------------------------------------------------------------------+ //| Return the close deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealOut(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Ticket() : 0); }
获取市价出场成交的指针,并返回其编号。
返回开仓时的点差:
//+------------------------------------------------------------------+ //| Return spread when opening | //+------------------------------------------------------------------+ int CPosition::SpreadIn(void) const { CDeal *deal=this.GetDealIn(); return(deal!=NULL ? deal.Spread() : 0); }
获取市价入场成交的指针,并返回其中设置的点差。
返回平仓时的点差:
//+------------------------------------------------------------------+ //| Return spread when closing | //+------------------------------------------------------------------+ int CPosition::SpreadOut(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Spread() : 0); }
获取市价出场成交的指针,并返回该成交订单中设置的点差。
返回平仓时的卖出价:
//+------------------------------------------------------------------+ //| Return Ask price when closing | //+------------------------------------------------------------------+ double CPosition::PriceOutAsk(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Ask() : 0); }
获取市价出场交易的指针,并返回其中设置的卖出价。
返回平仓时的买入价:
//+------------------------------------------------------------------+ //| Return the Bid price when closing | //+------------------------------------------------------------------+ double CPosition::PriceOutBid(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Bid() : 0); }
获取市价出场成交的指针,并返回该成交中设置的买入价数值。
返回点数利润的方法:
//+------------------------------------------------------------------+ //| Return a profit in points | //+------------------------------------------------------------------+ int CPosition::ProfitInPoints(void) const { //--- If symbol Point has not been received previously, inform of that and return 0 if(this.m_point==0) { ::Print("The Point() value could not be retrieved."); return 0; } //--- Get position open and close prices double open =this.PriceOpen(); double close=this.PriceClose(); //--- If failed to get the prices, return 0 if(open==0 || close==0) return 0; //--- Depending on the position type, return the calculated value of the position profit in points return (int)::round(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point); }
返回平仓时点差的方法:
//+------------------------------------------------------------------+ //| Return the spread value when closing | //+------------------------------------------------------------------+ double CPosition::SpreadOutCost(void) const { //--- Get close deal CDeal *deal=this.GetDealOut(); if(deal==NULL) return 0; //--- Get position profit and position profit in points double profit=this.Profit(); int profit_pt=this.ProfitInPoints(); //--- If the profit is zero, return the spread value using the TickValue * Spread * Lots equation if(profit==0) return(this.m_tick_value * deal.Spread() * deal.Volume()); //--- Calculate and return the spread value (proportion) return(profit_pt>0 ? deal.Spread() * ::fabs(profit / profit_pt) : 0); }
该方法使用两种方法来计算点差值:
- 如果仓位利润不等于零,则按以下比例计算点差成本:点差点数 * 仓位利润(货币)/ 仓位利润(点数)。
- 如果仓位利润为零,则使用以下公式计算点差值:计算出的每tick的价值 * 点差点数 * 成交数量。
设置所有订单总佣金的方法:
//+------------------------------------------------------------------+ //| Set the total commission for all deals | //+------------------------------------------------------------------+ void CPosition::SetCommissions(void) { double res=0; int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); res+=(deal!=NULL ? deal.Commission() : 0); } this.SetProperty(POSITION_PROP_COMMISSIONS, res); }
为了确定整个仓位生命周期内收取的佣金,我们需要将仓位中所有成交的佣金相加。在遍历仓位成交列表的循环中,将每个成交订单的佣金加到最终值上,该值最终会从方法中返回。
设置总成交费用的方法:
//+------------------------------------------------------------------+ //| Sets the total deal fee | //+------------------------------------------------------------------+ void CPosition::SetFee(void) { double res=0; int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { CDeal *deal=this.m_list_deals.At(i); res+=(deal!=NULL ? deal.Fee() : 0); } this.SetProperty(POSITION_PROP_FEE, res); }
这里与前面的方法完全相同——我们返回每个仓位成交订单的“Fee”值的总和。
这两个方法都必须在列出仓位的所有交易后调用,否则结果将不完整。
将成交订单添加到仓位成交列表中的方法:
//+------------------------------------------------------------------+ //| Add a deal to the list of deals | //+------------------------------------------------------------------+ CDeal *CPosition::DealAdd(const long ticket) { //--- A temporary object gets a ticket of the desired deal and the flag of sorting the list of deals by ticket this.m_temp_deal.SetTicket(ticket); this.m_list_deals.Sort(DEAL_PROP_TICKET); //--- Set the result of checking if a deal with such a ticket is present in the list bool exist=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE); //--- Return sorting by time in milliseconds for the list this.m_list_deals.Sort(DEAL_PROP_TIME_MSC); //--- If a deal with such a ticket is already in the list, return NULL if(exist) return NULL; //--- Create a new deal object CDeal *deal=new CDeal(ticket); if(deal==NULL) return NULL; //--- Add the created object to the list in sorting order by time in milliseconds //--- If failed to add the deal to the list, remove the the deal object and return NULL if(!this.m_list_deals.InsertSort(deal)) { delete deal; return NULL; } //--- If this is a position closing deal, set the profit from the deal properties to the position profit value if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { this.SetProfit(deal.Profit()); this.SetSwap(deal.Swap()); } //--- Return the pointer to the created deal object return deal; }
代码注释中完整描述了方法逻辑。该方法接收当前选定成交的价格数据。如果列表中还没有具有这样价格数据的成交,则创建一个新的成交订单对象并将其添加到仓位成交订单列表中。
返回某些仓位属性描述的方法:
//+------------------------------------------------------------------+ //| Return a position type description | //+------------------------------------------------------------------+ string CPosition::TypeDescription(void) const { return(this.TypePosition()==POSITION_TYPE_BUY ? "Buy" : this.TypePosition()==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.TypePosition()); } //+------------------------------------------------------------------+ //| Return position open time and price description | //+------------------------------------------------------------------+ string CPosition::TimePriceOpenDescription(void) { return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen())); } //+------------------------------------------------------------------+ //| Return position close time and price description | //+------------------------------------------------------------------+ string CPosition::TimePriceCloseDescription(void) { if(this.TimeCloseMsc()==0) return "Not closed yet"; return(::StringFormat("Closed %s [%.*f]", this.TimeMscToString(this.TimeCloseMsc()),this.m_digits, this.PriceClose())); } //+------------------------------------------------------------------+ //| Return a brief position description | //+------------------------------------------------------------------+ string CPosition::Description(void) { return(::StringFormat("%I64d (%s): %s %.2f %s #%I64d, Magic %I64d", this.AccountLogin(), this.AccountServer(), this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic())); }
这些方法用于,例如在日志中显示仓位描述。
打印方法允许在日志中显示仓位描述:
//+------------------------------------------------------------------+ //| Print the position properties and deals in the journal | //+------------------------------------------------------------------+ void CPosition::Print(void) { ::PrintFormat("%s\n-%s\n-%s", this.Description(), this.TimePriceOpenDescription(), this.TimePriceCloseDescription()); for(int i=0; i<this.m_list_deals.Total(); i++) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.Print(); } }
首先,打印一个包含仓位描述的标题。然后,通过遍历所有仓位的交易并使用每笔交易的Print()方法,打印每笔交易的描述。
历史仓位类已经准备好了。现在,让我们创建一个静态类,用于根据属性选择、搜索和排序成交和仓位。
根据交易和仓位属性进行搜索和排序的类
该类在文章《用于MetaTrader程序轻松快速开发的库(第三部分):市场订单和仓位集合,搜索和排序》中的“开展搜索”部分有详细讨论。
在\MQL5\Services\AccountReporter\目录下,创建CSelect类的新文件Select.mqh:
//+------------------------------------------------------------------+ //| Select.mqh | //| Copyright 2024, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| Class for sorting objects meeting the criterion | //+------------------------------------------------------------------+ class CSelect { }
设置比较模式的枚举,包含成交订单和仓位类的文件,以及声明存储列表:
//+------------------------------------------------------------------+ //| Select.mqh | //| Copyright 2024, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" enum ENUM_COMPARER_TYPE { EQUAL, // Equal MORE, // More LESS, // Less NO_EQUAL, // Not equal EQUAL_OR_MORE, // Equal or more EQUAL_OR_LESS // Equal or less }; //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "Deal.mqh" #include "Position.mqh" //+------------------------------------------------------------------+ //| Storage list | //+------------------------------------------------------------------+ CArrayObj ListStorage; // Storage object for storing sorted collection lists //+------------------------------------------------------------------+ //| Class for sorting objects meeting the criterion | //+------------------------------------------------------------------+ class CSelect { }
编写所有用于选择对象和创建满足搜索条件的列表的方法:
//+------------------------------------------------------------------+ //| Select.mqh | //| Copyright 2024, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" enum ENUM_COMPARER_TYPE // Comparison modes { EQUAL, // Equal MORE, // More LESS, // Less NO_EQUAL, // Not equal EQUAL_OR_MORE, // Equal or more EQUAL_OR_LESS // Equal or less }; //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "Deal.mqh" #include "Position.mqh" //+------------------------------------------------------------------+ //| Storage list | //+------------------------------------------------------------------+ CArrayObj ListStorage; // Storage object for storing sorted collection lists //+------------------------------------------------------------------+ //| Class for sorting objects meeting the criterion | //+------------------------------------------------------------------+ class CSelect { private: //--- Method for comparing two values template<typename T> static bool CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode); public: //+------------------------------------------------------------------+ //| Deal handling methods | //+------------------------------------------------------------------+ //--- Return the list of deals with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode); //--- Return the deal index with the maximum value of the (1) integer, (2) real and (3) string properties static int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property); static int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property); static int FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property); //--- Return the deal index with the minimum value of the (1) integer, (2) real and (3) string properties static int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property); static int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property); static int FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property); //+------------------------------------------------------------------+ //| Position handling methods | //+------------------------------------------------------------------+ //--- Return the list of positions with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode); static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode); //--- Return the position index with the maximum value of the (1) integer, (2) real and (3) string properties static int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property); static int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property); static int FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property); //--- Return the position index with the minimum value of the (1) integer, (2) real and (3) string properties static int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property); static int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property); static int FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property); }; //+------------------------------------------------------------------+ //| Method for comparing two values | //+------------------------------------------------------------------+ template<typename T> bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode) { switch(mode) { case EQUAL : return(value1==value2 ? true : false); case NO_EQUAL : return(value1!=value2 ? true : false); case MORE : return(value1>value2 ? true : false); case LESS : return(value1<value2 ? true : false); case EQUAL_OR_MORE : return(value1>=value2 ? true : false); case EQUAL_OR_LESS : return(value1<=value2 ? true : false); default : return false; } } //+------------------------------------------------------------------+ //| Deal list handling methods | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Return the list of deals with one integer | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT 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); if(!ListStorage.Add(list)) { delete list; return NULL; } int total=list_source.Total(); for(int i=0; i<total; i++) { CDeal *obj=list_source.At(i); long obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop, value, mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the list of deals with one real | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL 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); if(!ListStorage.Add(list)) { delete list; return NULL; } for(int i=0; i<list_source.Total(); i++) { CDeal *obj=list_source.At(i); double obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the list of deals with one string | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR 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); if(!ListStorage.Add(list)) { delete list; return NULL; } for(int i=0; i<list_source.Total(); i++) { CDeal *obj=list_source.At(i); string obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the maximum integer property value | //+------------------------------------------------------------------+ int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CDeal *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); long obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); long obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the maximum real property value | //+------------------------------------------------------------------+ int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CDeal *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); double obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); double obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the maximum string property value | //+------------------------------------------------------------------+ int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CDeal *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); string obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); string obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the minimum integer property value | //+------------------------------------------------------------------+ int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_INT property) { int index=0; CDeal *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); long obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); long obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the minimum real property value | //+------------------------------------------------------------------+ int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_DBL property) { int index=0; CDeal *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); double obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); double obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the deal index in the list | //| with the minimum string property value | //+------------------------------------------------------------------+ int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_STR property) { int index=0; CDeal *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CDeal *obj=list_source.At(i); string obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); string obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| Position list handling method | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Return the list of positions with one integer | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT 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); if(!ListStorage.Add(list)) { delete list; return NULL; } int total=list_source.Total(); for(int i=0; i<total; i++) { CPosition *obj=list_source.At(i); long obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop, value, mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the list of positions with one real | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL 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); if(!ListStorage.Add(list)) { delete list; return NULL; } for(int i=0; i<list_source.Total(); i++) { CPosition *obj=list_source.At(i); double obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the list of positions with one string | //| property meeting the specified criterion | //+------------------------------------------------------------------+ CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR 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); if(!ListStorage.Add(list)) { delete list; return NULL; } for(int i=0; i<list_source.Total(); i++) { CPosition *obj=list_source.At(i); string obj_prop=obj.GetProperty(property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } return list; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the maximum integer property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CPosition *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); long obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); long obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the maximum real property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CPosition *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); double obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); double obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the maximum string property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property) { if(list_source==NULL) return WRONG_VALUE; int index=0; CPosition *max_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); string obj1_prop=obj.GetProperty(property); max_obj=list_source.At(index); string obj2_prop=max_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the minimum integer property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_INT property) { int index=0; CPosition *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); long obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); long obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the minimum real property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_DBL property) { int index=0; CPosition *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); double obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); double obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } //+------------------------------------------------------------------+ //| Return the position index in the list | //| with the minimum string property value | //+------------------------------------------------------------------+ int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_STR property) { int index=0; CPosition *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CPosition *obj=list_source.At(i); string obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); string obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; }
请参阅上述文章的“开展搜索”部分。
现在我们已经准备好创建一个类来处理历史仓位列表。
历史仓位类的集合
在\MQL5\Services\AccountReporter\终端文件夹中,为CPositionsControl类创建一个新文件PositionsControl.mqh。
该类应该从标准库的CObject基类继承,同时需要将历史仓位类以及搜索和过滤类文件包含到正在创建的文件中。
//+------------------------------------------------------------------+ //| PositionsControl.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Position.mqh" #include "Select.mqh" //+------------------------------------------------------------------+ //| Collection class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { }
让我们声明类的私有、受保护和公共方法:
//+------------------------------------------------------------------+ //| Collection class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { private: //--- Return (1) position type and (2) reason for opening by deal type ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); ENUM_POSITION_REASON PositionReasonByDeal(const CDeal *deal); protected: CPosition m_temp_pos; // Temporary position object for searching CArrayObj m_list_pos; // List of positions //--- Return the position object from the list by ID CPosition *GetPositionObjByID(const long id); //--- Return the flag of the market position bool IsMarketPosition(const long id); public: //--- Create and update the list of positions. It can be redefined in the inherited classes virtual bool Refresh(void); //--- Return (1) the list, (2) number of positions in the list CArrayObj *GetPositionsList(void) { return &this.m_list_pos; } int PositionsTotal(void) const { return this.m_list_pos.Total(); } //--- Print the properties of all positions and their deals in the journal void Print(void); //--- Constructor/destructor CPositionsControl(void); ~CPositionsControl(); };
我们来研究一下所声明方法的实现。
在类的构造函数中,设置历史仓位列表按关闭时间的毫秒数排序的标志:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPositionsControl::CPositionsControl(void) { this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC); }
在类的析构函数中,销毁历史仓位列表:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPositionsControl::~CPositionsControl() { this.m_list_pos.Shutdown(); }
通过ID从列表中返回头寸对象指针的方法:
//+------------------------------------------------------------------+ //| Return the position object from the list by ID | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetPositionObjByID(const long id) { //--- Set the position ID for the temporary object and set the flag of sorting by position ID for the list this.m_temp_pos.SetID(id); this.m_list_pos.Sort(POSITION_PROP_IDENTIFIER); //--- Get the index of the position object with the specified ID (or -1 if it is absent) from the list //--- Use the obtained index to get the pointer to the positino object from the list (or NULL if the index value is -1) int index=this.m_list_pos.Search(&this.m_temp_pos); CPosition *pos=this.m_list_pos.At(index); //--- Return the flag of sorting by position close time in milliseconds for the list and //--- return the pointer to the position object (or NULL if it is absent) this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC); return pos; }
返回市场仓位标志的方法:
//+------------------------------------------------------------------+ //| Return the market position flag | //+------------------------------------------------------------------+ bool CPositionsControl::IsMarketPosition(const long id) { //--- In a loop by the list of current positions in the terminal for(int i=::PositionsTotal()-1; i>=0; i--) { //--- get the position ticket by the loop index ulong ticket=::PositionGetTicket(i); //--- If the ticket is received, the position can be selected and its ID is equal to the one passed to the method, //--- this is the desired market position, return 'true' if(ticket!=0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id) return true; } //--- No such market position, return 'false' return false; }
通过交易类型返回仓位类型的方法:
//+------------------------------------------------------------------+ //| Return position type by deal type | //+------------------------------------------------------------------+ ENUM_POSITION_TYPE CPositionsControl::PositionTypeByDeal(const CDeal *deal) { if(deal==NULL) return WRONG_VALUE; switch(deal.TypeDeal()) { case DEAL_TYPE_BUY : return POSITION_TYPE_BUY; case DEAL_TYPE_SELL : return POSITION_TYPE_SELL; default : return WRONG_VALUE; } }
根据成交类型,返回相应的仓位类型。
返回按成交类型开仓原因的方法:
//+------------------------------------------------------------------+ //| Returns the reason for opening a position by deal type | //+------------------------------------------------------------------+ ENUM_POSITION_REASON CPositionsControl::PositionReasonByDeal(const CDeal *deal) { if(deal==NULL) return WRONG_VALUE; switch(deal.Reason()) { case DEAL_REASON_CLIENT : return POSITION_REASON_CLIENT; case DEAL_REASON_MOBILE : return POSITION_REASON_MOBILE; case DEAL_REASON_WEB : return POSITION_REASON_WEB; case DEAL_REASON_EXPERT : return POSITION_REASON_EXPERT; default : return WRONG_VALUE; } }
根据开仓原因,返回相应的开仓原因。
创建或更新历史仓位列表的方法:
//+------------------------------------------------------------------+ //| Create historical position list | //+------------------------------------------------------------------+ bool CPositionsControl::Refresh(void) { //--- If failed to request the history of deals and orders, return 'false' if(!::HistorySelect(0,::TimeCurrent())) return false; //--- Set the flag of sorting by time in milliseconds for the position list this.m_list_pos.Sort(POSITION_PROP_TIME_MSC); //--- Declare a result variable and a pointer to the position object bool res=true; CPosition *pos=NULL; //--- In a loop based on the number of history deals int total=::HistoryDealsTotal(); for(int i=total-1; i>=0; i--) { //--- get the ticket of the next deal in the list ulong ticket=::HistoryDealGetTicket(i); //--- If the deal ticket is not received, or it is not a buy/sell deal, move on ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE); if(ticket==0 || (deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL)) continue; //--- Get the value of the position ID from the deal long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID); //--- If this is a market position, move on if(this.IsMarketPosition(pos_id)) continue; //--- Get the pointer to a position object from the list pos=this.GetPositionObjByID(pos_id); //--- If there is no position with this ID in the list yet if(pos==NULL) { //--- Create a new position object and, if the object could not be created, add 'false' to the 'res' variable and move on string pos_symbol=HistoryDealGetString(ticket, DEAL_SYMBOL); pos=new CPosition(pos_id, pos_symbol); if(pos==NULL) { res &=false; continue; } //--- If failed to add the position object to the list, add 'false' to the 'res' variable, remove the position object and move on if(!this.m_list_pos.InsertSort(pos)) { res &=false; delete pos; continue; } } //--- If the deal object could not be added to the list of deals of the position object, add 'false' to the 'res' variable and move on CDeal *deal=pos.DealAdd(ticket); if(deal==NULL) { res &=false; continue; } //--- All is successful. //--- Set position properties depending on the deal type if(deal.Entry()==DEAL_ENTRY_IN) { pos.SetTicket(deal.Order()); pos.SetMagic(deal.Magic()); pos.SetTime(deal.Time()); pos.SetTimeMsc(deal.TimeMsc()); ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); ENUM_POSITION_REASON reason=this.PositionReasonByDeal(deal); pos.SetReason(reason); pos.SetPriceOpen(deal.Price()); pos.SetVolume(deal.Volume()); } if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { pos.SetPriceCurrent(deal.Price()); pos.SetPriceClose(deal.Price()); pos.SetTimeClose(deal.Time()); pos.SetTimeCloseMsc(deal.TimeMsc()); } if(deal.Entry()==DEAL_ENTRY_INOUT) { ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetVolume(deal.Volume()-pos.Volume()); } } //--- All historical positions are created and the corresponding deals are added to the deal lists of the position objects //--- Set the flag of sorting by close time in milliseconds for the position list this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC); //--- In the loop through the created list of closed positions, we set the Commissions and Fee values for each position for(int i=0; i<this.m_list_pos.Total(); i++) { CPosition *pos=this.m_list_pos.At(i); if(pos==NULL) continue; pos.SetCommissions(); pos.SetFee(); } //--- Return the result of creating and adding a position to the list return res; }
遍历终端中的成交列表,我们获取下一个成交并检查其仓位ID。如果这是一个市价仓位,则跳过该成交订单。如果这样的仓位尚不在历史仓位列表中,则创建一个新的仓位对象并将其放置在历史仓位列表中。如果历史仓位对象中还没有与所选成交订单单号相对应的成交订单,则将其添加到仓位对象的成交订单列表中。在为每个仓位创建历史仓位对象的循环结束时,为所有仓位的成交订单设置一个共同的佣金和交易费用。该方法是虚拟的,这允许我们在继承类中创建更优化的逻辑,如果更新仓位列表的频率远高于每天至少一次的话。
用于在日志中打印头寸及其交易属性的方法:
//+------------------------------------------------------------------+ //| Print the properties of positions and their deals in the journal | //+------------------------------------------------------------------+ void CPositionsControl::Print(void) { int total=this.m_list_pos.Total(); for(int i=0; i<total; i++) { CPosition *pos=this.m_list_pos.At(i); if(pos==NULL) continue; pos.Print(); } }
如果我们需要控制已创建的历史仓位列表,此方法允许我们在日志中显示每个仓位及其成交订单。
服务应用程序“记住”在连续服务运行期间连接过的所有账户。换句话说,如果没有终端重启,并且连接了不同的账户和交易服务器,那么程序会记住这些账户,而这些账户又会存储所有已平仓位的列表。为每个已连接的账户上显示过的已平仓位显示交易报告。或者,如果设置要求仅显示当前账户的报告,那么,已平仓位的列表会根据当前账户的登录名和服务器进行排序。
基于以上内容,我们需要一个账户类来存储该账户上交易的所有已平仓位列表的管理类。在服务应用程序中,我们将获取所需的账户,该账户将用于检索已平仓位的列表。
账户类
在\MQL5\ServicesAccountReporter中,创建CAccount类的新文件Account.mqh。
该类应该从标准库的CObject基类继承,同时需要将历史仓位集合类文件包含到创建的文件中:
//+------------------------------------------------------------------+ //| Account.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "PositionsControl.mqh" //+------------------------------------------------------------------+ //| Account class | //+------------------------------------------------------------------+ class CAccount : public CObject { }
在类的受保护部分,声明历史仓位控制对象(已平账户仓位列表的类)以及整数、浮点数和字符串属性的列表:
//+------------------------------------------------------------------+ //| Account class | //+------------------------------------------------------------------+ class CAccount : public CObject { private: protected: CPositionsControl m_positions; // Historical positions control object //--- account integer properties long m_login; // Account number ENUM_ACCOUNT_TRADE_MODE m_trade_mode; // Trading account type long m_leverage; // Leverage int m_limit_orders; // Maximum allowed number of active pending orders ENUM_ACCOUNT_STOPOUT_MODE m_margin_so_mode; // Mode of setting the minimum available margin level bool m_trade_allowed; // Trading permission of the current account bool m_trade_expert; // Trading permission of an EA ENUM_ACCOUNT_MARGIN_MODE m_margin_mode; // Margin calculation mode int m_currency_digits; // Number of digits for an account currency necessary for accurate display of trading results bool m_fifo_close; // The flag indicating that positions can be closed only by the FIFO rule bool m_hedge_allowed; // Allowed opposite positions on a single symbol //--- account real properties double m_balance; // Account balance in a deposit currency double m_credit; // Credit in a deposit currency double m_profit; // Current profit on an account in the account currency double m_equity; // Equity on an account in the deposit currency double m_margin; // Reserved margin on an account in a deposit currency double m_margin_free; // Free funds available for opening a position in a deposit currency double m_margin_level; // Margin level on an account in % double m_margin_so_call; // Margin Call level double m_margin_so_so; // Stop Out level double m_margin_initial; // Funds reserved on an account to ensure a guarantee amount for all pending orders double m_margin_maintenance; // Funds reserved on an account to ensure a minimum amount for all open positions double m_assets; // Current assets on an account double m_liabilities; // Current liabilities on an account double m_commission_blocked; // Current sum of blocked commissions on an account //--- account string properties string m_name; // Client name string m_server; // Trade server name string m_currency; // Deposit currency string m_company; // Name of a company serving account public:
在公共部分,设置处理列表的方法,设置和返回账户对象属性的方法以及其他方法:
public: //--- Return the (1) control object, (2) the list of historical positions, (3) number of positions CPositionsControl*GetPositionsCtrlObj(void) { return &this.m_positions; } CArrayObj *GetPositionsList(void) { return this.m_positions.GetPositionsList();} int PositionsTotal(void) { return this.m_positions.PositionsTotal(); } //--- Return the list of positions by (1) integer, (2) real and (3) string property CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_INT property, long value, ENUM_COMPARER_TYPE mode) { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); } CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_DBL property, double value, ENUM_COMPARER_TYPE mode) { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); } CArrayObj *GetPositionsList(ENUM_POSITION_PROPERTY_STR property, string value, ENUM_COMPARER_TYPE mode) { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); } //--- (1) Update and (2) print the list of closed positions in the journal bool PositionsRefresh(void) { return this.m_positions.Refresh();} void PositionsPrint(void) { this.m_positions.Print(); } //--- set (1) login and (2) server void SetLogin(const long login) { this.m_login=login; } void SetServer(const string server) { this.m_server=server; } //--- return integer account properties long Login(void) const { return this.m_login; } // Account number ENUM_ACCOUNT_TRADE_MODE TradeMode(void) const { return this.m_trade_mode; } // Trading account type long Leverage(void) const { return this.m_leverage; } // Provided leverage int LimitOrders(void) const { return this.m_limit_orders; } // Maximum allowed number of active pending orders ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(void) const { return this.m_margin_so_mode; } // Mode of setting the minimum available margin level bool TradeAllowed(void) const { return this.m_trade_allowed; } // Trading permission of the current account bool TradeExpert(void) const { return this.m_trade_expert; } // Trading permission for EA ENUM_ACCOUNT_MARGIN_MODE MarginMode(void) const { return this.m_margin_mode; } // Margin calculation mode int CurrencyDigits(void) const { return this.m_currency_digits; } // Number of digits for an account currency necessary for accurate display of trading results bool FIFOClose(void) const { return this.m_fifo_close; } // The flag indicating that positions can be closed only by the FIFO rule bool HedgeAllowed(void) const { return this.m_hedge_allowed; } // Allowed opposite positions on a single symbol //--- return real account properties double Balance(void) const { return this.m_balance; } // Account balance in a deposit currency double Credit(void) const { return this.m_credit; } // Credit in deposit currency double Profit(void) const { return this.m_profit; } // Current profit on an account in the account currency double Equity(void) const { return this.m_equity; } // Available equity in the deposit currency double Margin(void) const { return this.m_margin; } // The amount of reserved collateral funds on the account in the deposit currency double MarginFree(void) const { return this.m_margin_free; } // Free funds available for opening a position in a deposit currency double MarginLevel(void) const { return this.m_margin_level; } // Margin level on an account in % double MarginSoCall(void) const { return this.m_margin_so_call; } // Margin Call level double MarginSoSo(void) const { return this.m_margin_so_so; } // Stop Out level double MarginInitial(void) const { return this.m_margin_initial; } // Funds reserved on an account to ensure a guarantee amount for all pending orders double MarginMaintenance(void) const { return this.m_margin_maintenance; } // Funds reserved on an account to ensure the minimum amount for all open positions double Assets(void) const { return this.m_assets; } // Current assets on an account double Liabilities(void) const { return this.m_liabilities; } // Current amount of liabilities on the account double CommissionBlocked(void) const { return this.m_commission_blocked; } // Current sum of blocked commissions on an account //--- return account string properties string Name(void) const { return this.m_name; } // Client name string Server(void) const { return this.m_server; } // Trade server name string Currency(void) const { return this.m_currency; } // Deposit currency string Company(void) const { return this.m_company; } // Name of the company servicing the account //--- return (1) account description, (2) trading account type and (3) margin calculation mode string Description(void) const; string TradeModeDescription(void) const; string MarginModeDescription(void)const; //--- virtual method for comparing two objects virtual int Compare(const CObject *node,const int mode=0) const; //--- Display the account description in the journal void Print(void) { ::Print(this.Description()); } //--- constructors/destructor CAccount(void){} CAccount(const long login, const string server_name); ~CAccount() {} };
我们来研究一下所声明方法的实现。
在类的构造函数中,将当前账户的所有属性设置到对象中:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CAccount::CAccount(const long login, const string server_name) { this.m_login=login; this.m_server=server_name; //--- set account integer properties this.m_trade_mode = (ENUM_ACCOUNT_TRADE_MODE)::AccountInfoInteger(ACCOUNT_TRADE_MODE); // Trading account type this.m_leverage = ::AccountInfoInteger(ACCOUNT_LEVERAGE); // Leverage this.m_limit_orders = (int)::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS); // Maximum allowed number of active pending orders this.m_margin_so_mode = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);// Mode of setting the minimum available margin level this.m_trade_allowed = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); // Trading permission of the current account this.m_trade_expert = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT); // Trading permission of an EA this.m_margin_mode = (ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE); // Margin calculation mode this.m_currency_digits = (int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS); // Number of digits for an account currency necessary for accurate display of trading results this.m_fifo_close = ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE); // The flag indicating that positions can be closed only by the FIFO rule this.m_hedge_allowed = ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED); // Allowed opposite positions on a single symbol //--- set account real properties this.m_balance = ::AccountInfoDouble(ACCOUNT_BALANCE); // Account balance in a deposit currency this.m_credit = ::AccountInfoDouble(ACCOUNT_CREDIT); // Credit in a deposit currency this.m_profit = ::AccountInfoDouble(ACCOUNT_PROFIT); // Current profit on an account in the account currency this.m_equity = ::AccountInfoDouble(ACCOUNT_EQUITY); // Equity on an account in the deposit currency this.m_margin = ::AccountInfoDouble(ACCOUNT_MARGIN); // Reserved margin on an account in a deposit currency this.m_margin_free = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE); // Free funds available for opening a position in a deposit currency this.m_margin_level = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); // Margin level on an account in % this.m_margin_so_call = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL); // Margin Call level this.m_margin_so_so = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO); // Stop Out level this.m_margin_initial = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL); // Funds reserved on an account to ensure a guarantee amount for all pending orders this.m_margin_maintenance = ::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE); // Funds reserved on an account to ensure a minimum amount for all open positions this.m_assets = ::AccountInfoDouble(ACCOUNT_ASSETS); // Current assets on an account this.m_liabilities = ::AccountInfoDouble(ACCOUNT_LIABILITIES); // Current liabilities on an account this.m_commission_blocked = ::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED); // Current sum of blocked commissions on an account //--- set account string properties this.m_name = ::AccountInfoString(ACCOUNT_NAME); // Client name this.m_currency = ::AccountInfoString(ACCOUNT_CURRENCY); // Deposit currency this.m_company = ::AccountInfoString(ACCOUNT_COMPANY); // Name of a company serving account }
比较两个对象的方法:
//+------------------------------------------------------------------+ //| Method for comparing two objects | //+------------------------------------------------------------------+ int CAccount::Compare(const CObject *node,const int mode=0) const { const CAccount *obj=node; return(this.Login()>obj.Login() ? 1 : this.Login()<obj.Login() ? -1 : this.Server()>obj.Server() ? 1 : this.Server()<obj.Server() ? -1 : 0); }
该方法仅通过两个属性——登录名和服务器名称——来比较两个账户对象。如果两个被比较对象的登录名相同,则会检查服务器名称是否相同。如果服务器也相同,则这两个对象相等。否则,根据两个对象之间正在比较的属性的值是较大还是较小,返回1或-1。
返回某些账户对象属性描述的方法:
//+------------------------------------------------------------------+ //| Return the description of the trading account type | //+------------------------------------------------------------------+ string CAccount::TradeModeDescription(void) const { string mode=::StringSubstr(::EnumToString(this.TradeMode()), 19); if(mode.Lower()) mode.SetChar(0, ushort(mode.GetChar(0)-32)); return mode; } //+------------------------------------------------------------------+ //| Return the description of the margin calculation mode | //+------------------------------------------------------------------+ string CAccount::MarginModeDescription(void) const { string mode=::StringSubstr(::EnumToString(this.MarginMode()), 20); ::StringReplace(mode, "RETAIL_", ""); if(mode.Lower()) mode.SetChar(0, ushort(mode.GetChar(0)-32)); return mode; }
这些方法用于在Description 方法中创建账户描述:
//+------------------------------------------------------------------+ //| Return the account description | //+------------------------------------------------------------------+ string CAccount::Description(void) const { return(::StringFormat("%I64d: %s (%s, %s, %.2f %s, %s)", this.Login(), this.Name(), this.Company(), this.TradeModeDescription(), this.Balance(), this.Currency(), this.MarginModeDescription())); }
方法返回一个字符串如下:
68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
这个字符串可以使用类的 Print() 方法打印到日志中。
现在我们需要创建一个类,用于存储服务应用程序运行过程中连接的所有账户的列表。
账户类的集合
在MetaTrader 5终端的\MT5\MQL5\Services\AccountReporter\文件夹中,为CAccounts类创建一个名为Accounts.mqh的新文件。
该类应该继承自标准库的 CObject 基类,同时在创建的文件中包含账户类的文件。
//+------------------------------------------------------------------+ //| Accounts.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Account.mqh" //+------------------------------------------------------------------+ //| Account collection class | //+------------------------------------------------------------------+ class CAccounts : public CObject { }
在类的私有、受保护和公共部分中声明操作方法:
//+------------------------------------------------------------------+ //| Account collection class | //+------------------------------------------------------------------+ class CAccounts : public CObject { private: CArrayObj m_list; // List of account objects CAccount m_tmp; // Temporary account object for searching protected: //--- Create a new account object and add it to the list CAccount *Add(const long login, const string server); public: //--- Create a new account object bool Create(const long login, const string server); //--- Return the pointer to the specified account object by (1) login and server, (2) index in the list CAccount *Get(const long login, const string server); CAccount *Get(const int index) const { return this.m_list.At(index); } //--- Combine the lists of account positions and return the combined one CArrayObj *GetCommonPositionsList(void); //--- Return the list of positions for the specified account CArrayObj *GetAccountPositionsList(const long login, const string server); //--- Return the number of stored accounts int Total(void) const { return this.m_list.Total(); } //--- Update the lists of positions of the specified account bool PositionsRefresh(const long login, const string server); //--- Constructor/destructor CAccounts(); ~CAccounts(); };
我们来研究一下所声明方法的实现。
在类的构造函数中,为账户列表设置排序标识:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CAccounts::CAccounts() { this.m_list.Sort(); }
在类的析构函数中清理账户列表:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CAccounts::~CAccounts() { this.m_list.Clear(); }
受保护的方法,用于创建新的账户对象并将其添加到列表中。
//+------------------------------------------------------------------+ //| Create a new account object and add it to the list | //+------------------------------------------------------------------+ CAccount *CAccounts::Add(const long login,const string server) { //--- Create a new account object CAccount *account=new CAccount(login, server); if(account==NULL) return NULL; //--- If the created object is not added to the list, remove it and return NULL if(!this.m_list.Add(account)) { delete account; return NULL; } //--- Return the pointer to a created object return account; }
这是一个受保护的方法,它是用于 创建新账户对象的公共方法的一部分。
//+------------------------------------------------------------------+ //| Create a new account object | //+------------------------------------------------------------------+ bool CAccounts::Create(const long login,const string server) { //--- Set login and server to the temporary account object this.m_tmp.SetLogin(login); this.m_tmp.SetServer(server); //--- Set the sorted list flag for the account object list //--- and get the object index having the same login and server as the ones the temporary object has this.m_list.Sort(); int index=this.m_list.Search(&this.m_tmp); //--- Return the flag of an object being successfully added to the list (Add method operation result) or 'false' if the object is already in the list return(index==WRONG_VALUE ? this.Add(login, server)!=NULL : false); }
返回特定账户对象指针的方法:
//+------------------------------------------------------------------+ //| Return the pointer to the specified account object | //+------------------------------------------------------------------+ CAccount *CAccounts::Get(const long login,const string server) { //--- Set login and server to the temporary account object this.m_tmp.SetLogin(login); this.m_tmp.SetServer(server); //--- Set the sorted list flag for the account object list //--- and get the object index having the same login and server as the ones the temporary object has this.m_list.Sort(); int index=this.m_list.Search(&this.m_tmp); //--- Return the pointer to the object in the list by index or NULL if the index is -1 return this.m_list.At(index); }
更新特定账户的头寸列表的方法:
//+------------------------------------------------------------------+ //| Update the lists of positions of the specified account | //+------------------------------------------------------------------+ bool CAccounts::PositionsRefresh(const long login, const string server) { //--- Get the pointer to the account object with the specified login and server CAccount *account=this.Get(login, server); if(account==NULL) return false; //--- If the received object is not the current account, if(account.Login()!=::AccountInfoInteger(ACCOUNT_LOGIN) || account.Server()!=::AccountInfoString(ACCOUNT_SERVER)) { //--- inform that updating data of the non-current account will result in incorrect data and return 'false' ::Print("Error. Updating the list of positions for a non-current account will result in incorrect data."); return false; } //--- Return the result of updating the current account data return account.PositionsRefresh(); }
将账户持仓列表合并并返回一个合并后的列表的方法:
//+--------------------------------------------------------------------+ //| Combine the lists of account positions and return the combined one | //+--------------------------------------------------------------------+ CArrayObj *CAccounts::GetCommonPositionsList(void) { //--- Create a new list and reset the flag of managing memory CArrayObj *list=new CArrayObj(); if(list==NULL) return NULL; list.FreeMode(false); //--- In the loop through the list of accounts, int total=this.m_list.Total(); for(int i=0; i<total; i++) { //--- get another account object CAccount *account=this.m_list.At(i); if(account==NULL) continue; //--- Get the list of closed account positions CArrayObj *src=account.GetPositionsList(); if(src==NULL) continue; //--- If this is the first account in the list, if(i==0) { //--- copy the elements from the account positions list to the new list if(!list.AssignArray(src)) { delete list; return NULL; } } //--- If this is not the first account in the list, else { //--- add elements from the account position list to the end of the new list if(!list.AddArray(src)) continue; } } //--- Send a new list to the storage if(!ListStorage.Add(list)) { delete list; return NULL; } //--- Return the pointer to the created and filled list return list; }
返回指定账户持仓列表的方法:
//+------------------------------------------------------------------+ //| Return the list of positions for the specified account | //+------------------------------------------------------------------+ CArrayObj *CAccounts::GetAccountPositionsList(const long login,const string server) { CAccount *account=this.Get(login, server); return(account!=NULL ? account.GetPositionsList() : NULL); }
通过登录信息和服务器获取账户对象的指针,并返回其历史持仓列表的指针;如果无法获取账户对象,则返回NULL。
此类中的所有方法都在注释中进行了详细描述。如果仍有不清楚的地方,您可以在文章讨论中提出疑问。
构成服务应用程序基础的所有类都已准备就绪。让我们开始实现程序本身。
用于创建交易报告和发送通知的服务应用程序
让我们决定程序应该如何工作。
当服务启动时,会检查客户端终端中是否存在MetaQuotes ID以及是否允许向智能手机发送推送通知。
这些设置可以在“工具”--“选项”菜单的“通知”选项卡中找到:
如果MetaQuotes ID字段中没有值,或者未选中“启用推送通知”复选框,服务将显示一个窗口,要求您设置这些参数。如果您选择不设置这些参数,系统将发出警告,提示没有MQID或不允许向智能手机发送通知,并且所有消息将仅记录在日志中。如果我们设置了所有参数,报告将同时发送到智能手机和专家终端日志。在主循环中,程序将不断检查终端中通知发送设置的状态。因此,如果在启动服务时未设置发送通知的权限,我们可以在启动服务程序后随时启用它——程序将看到更改并启用相应的标志。
在服务设置中,我们将能够选择需要发送消息的参数和需要生成报告的时间段:
- 通用报告参数
- 用于报告的账户:(全部或当前),
- 是否按交易品种创建报告:(是/否)——首先创建一个报告,然后为交易中涉及的每个交易品种从中创建单独的报告,
- 是否按magic编号创建报告:(是/否)——创建一个报告,然后为交易中使用的每个magic编号从中创建单独的报告,
- 报告中是否包含佣金:(是/否)——如果启用,除了所有成本的总额外,佣金、掉期和交易费用的成本将单独显示,
- 报告中是否包含平仓时可能的点差损失:(是/否)——如果启用,平仓时所有可能的点差费用总和将单独显示;
- 每日报告设置
- 是否发送过去24小时的报告;这也适用于指定时间段的报告:(是/否)——如果启用,则每天将在指定时间发送过去24小时和可配置的交易时间间隔(天数、月数和年数)的报告,
- 报告发送小时:(默认8),
- 报告发送分钟:(默认0);
- 自定义时间段的每日报告设置
- 是否发送指定天数的报告:(是/否)——如果启用,则每天将在上面指定的时间创建指定天数的报告;报告的天数是通过从当前日期减去指定的天数来计算的,
- 指定报告的天数:(默认7),
- 是否发送指定月数的报告:(是/否)——如果启用,则每天将在上面指定的时间创建指定月数的报告;报告的月数是通过从当前日期减去指定的月数来计算的,
- 指定月数报告的月数:(默认3),
- 是否发送指定年数的报告:(是/否)——如果启用,则每天将在上面指定的时间创建指定年数的报告;报告的年数是通过从当前日期减去指定的年数来计算的,
- 指定年数报告的年数:(默认2);
- 所有其他时间段的周报告设置
- 发送周报的星期几:(默认为星期六)——当指定日期到来时,将创建并发送下面设置中指定的报告。
- 报告发送小时:(默认8),
- 报告发送分钟:(默认0);
- 是否从当前周初开始发送报告:(是/否) —— 如果启用,则每周在指定日期创建一份从当前周初开始的报告,
- 是否从当前月初开始发送报告:(是/否) —— 如果启用,则每周在指定日期创建一份从当前月初开始的报告,
- 是否从当前年初开始发送报告:(是/否) —— 如果启用,则每周在指定日期创建一份从当前年初开始的报告,
- 是否发送整个交易周期的报告:(是/否) —— 如果启用,则每周在指定日期创建一份整个交易周期的报告。
这些设置将足以涵盖创建报告所需关注的大多数交易周期。
在\MQL5\Services\AccountReporter\终端文件夹中,创建新的服务应用程序文件Reporter.mq5
让我们输入必要的宏替换,连接外部文件,编写枚举、输入参数和全局变量,以便程序能够运行:
//+------------------------------------------------------------------+ //| Reporter.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property service #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define COUNTER_DELAY 1000 // Counter delay in milliseconds during the working loop #define REFRESH_ATTEMPTS 5 // Number of attempts to obtain correct account data #define REFRESH_DELAY 500 // Delay in milliseconds before next attempt to get data #define TABLE_COLUMN_W 10 // Width of the statistics table column for displaying in the journal #include <Arrays\ArrayString.mqh> // Dynamic array of string variables for a symbol list object #include <Arrays\ArrayLong.mqh> // Dynamic array of long type variables for the magic number list object #include <Tools\DateTime.mqh> // Expand the MqlDateTime structure #include "Accounts.mqh" // Collection class of account objects //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_USED_ACCOUNTS // Enumerate used accounts in statistics { USED_ACCOUNT_CURRENT, // Current Account only USED_ACCOUNTS_ALL, // All used accounts }; enum ENUM_REPORT_RANGE // Enumerate statistics ranges { REPORT_RANGE_DAILY, // Day REPORT_RANGE_WEEK_BEGIN, // Since the beginning of the week REPORT_RANGE_MONTH_BEGIN, // Since the beginning of the month REPORT_RANGE_YEAR_BEGIN, // Since the beginning of the year REPORT_RANGE_NUM_DAYS, // Number of days REPORT_RANGE_NUM_MONTHS, // Number of months REPORT_RANGE_NUM_YEARS, // Number of years REPORT_RANGE_ALL, // Entire period }; enum ENUM_REPORT_BY // Enumerate statistics filters { REPORT_BY_RANGE, // Date range REPORT_BY_SYMBOLS, // By symbols REPORT_BY_MAGICS, // By magic numbers }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "============== Report options ==============" input ENUM_USED_ACCOUNTS InpUsedAccounts = USED_ACCOUNT_CURRENT;// Accounts included in statistics input bool InpReportBySymbols = true; // Reports by Symbol input bool InpReportByMagics = true; // Reports by Magics input bool InpCommissionsInclude= true; // Including Comissions input bool InpSpreadInclude = true; // Including Spread input group "========== Daily reports for daily periods ==========" input bool InpSendDReport = true; // Send daily report (per day and specified periods) input uint InpSendDReportHour = 8; // Hour of sending the report (Local time) input uint InpSendDReportMin = 0; // Minutes of sending the report (Local time) input group "========= Daily reports for specified periods =========" input bool InpSendSReportDays = true; // Send a report for the specified num days input uint InpSendSReportDaysN = 7; // Number of days to report for the specified number of days input bool InpSendSReportMonths = true; // Send a report for the specified num months input uint InpSendSReportMonthsN= 3; // Number of months to report for the specified number of months input bool InpSendSReportYears = true; // Send a report for the specified num years input uint InpSendSReportYearN = 2; // Number of years to report for the specified number of years input group "======== Weekly reports for all other periods ========" input ENUM_DAY_OF_WEEK InpSendWReportDayWeek= SATURDAY; // Day of sending the reports (Local time) input uint InpSendWReportHour = 8; // Hour of sending the reports (Local time) input uint InpSendWReportMin = 0; // Minutes of sending the reports (Local time) input bool InpSendWReport = true; // Send a report for the current week input bool InpSendMReport = false; // Send a report for the current month input bool InpSendYReport = false; // Send a report for the current year input bool InpSendAReport = false; // Send a report for the entire trading period //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CAccounts ExtAccounts; // Account management object long ExtLogin; // Current account login string ExtServer; // Current account server bool ExtNotify; // Push notifications enabling flag //+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { }
我们可以看到包含了\MQL5\Include\ToolsDateTime.mqh文件。这是一个从标准MqlDateTime继承而来的结构体:
//+------------------------------------------------------------------+ //| DateTime.mqh | //| Copyright 2000-2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Structure CDateTime. | //| Purpose: Working with dates and time. | //| Extends the MqlDateTime structure. | //+------------------------------------------------------------------+ struct CDateTime : public MqlDateTime { //--- additional information string MonthName(const int num) const; string ShortMonthName(const int num) const; string DayName(const int num) const; string ShortDayName(const int num) const; string MonthName(void) const { return(MonthName(mon)); } string ShortMonthName(void) const { return(ShortMonthName(mon)); } string DayName(void) const { return(DayName(day_of_week)); } string ShortDayName(void) const { return(ShortDayName(day_of_week)); } int DaysInMonth(void) const; //--- data access datetime DateTime(void) { return(StructToTime(this)); } void DateTime(const datetime value) { TimeToStruct(value,this); } void DateTime(const MqlDateTime& value) { this=value; } void Date(const datetime value); void Date(const MqlDateTime &value); void Time(const datetime value); void Time(const MqlDateTime &value); //--- settings void Sec(const int value); void Min(const int value); void Hour(const int value); void Day(const int value); void Mon(const int value); void Year(const int value); //--- increments void SecDec(int delta=1); void SecInc(int delta=1); void MinDec(int delta=1); void MinInc(int delta=1); void HourDec(int delta=1); void HourInc(int delta=1); void DayDec(int delta=1); void DayInc(int delta=1); void MonDec(int delta=1); void MonInc(int delta=1); void YearDec(int delta=1); void YearInc(int delta=1); //--- check void DayCheck(void); };
该结构体包含了用于处理日期和时间的现成方法。我们需要计算统计周期的起始时间。这正是结构体方法派上用场的地方,以便在从当前日期减去天数、周数、月数和年数时,不必自行计算所得日期的有效性。在这里,所有计算都会进行错误值的校正。例如,如果从当前日期中减去的天数超过了该月的天数,那么应该通过计算月份和日期来调整所得日期,同时考虑到闰年。但是,更简单的做法是直接使用给定结构体的减少天数、月份和年份的方法,以立即获得正确的最终日期。
服务应用程序本身应该在一个无限循环中运行。在循环中,设置大约一秒钟的延迟。等待结束后,进行所有检查和计算。为了清晰和更好地理解,整个循环体被分为带有标题的代码块。让我们来看看程序本身的主体:
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { //--- CArrayObj *PositionsList = NULL; // List of closed account positions long account_prev = 0; // Previous login double balance_prev = EMPTY_VALUE; // Previous balance bool Sent = false; // Flag of sent report for non-daily periods int day_of_year_prev= WRONG_VALUE; // The previous day number of the year //--- Create lists of symbols and magic numbers traded in history and a list of messages for Push notifications CArrayString *SymbolsList = new CArrayString(); CArrayLong *MagicsList = new CArrayLong(); CArrayString *MessageList = new CArrayString(); if(SymbolsList==NULL || MagicsList==NULL || MessageList==NULL) { Print("Failed to create list CArrayObj"); return; } //--- Check for the presence of MetaQuotes ID and permission to send notifications to it ExtNotify=CheckMQID(); if(ExtNotify) Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK"); //--- The main loop int count=0; while(!IsStopped()) { //+------------------------------------------------------------------+ //| Delay in the loop | //+------------------------------------------------------------------+ //--- Increase the loop counter. If the counter has not exceeded the specified value, repeat Sleep(16); count+=10; if(count<COUNTER_DELAY) continue; //--- Waiting completed. Reset the loop counter count=0; //+------------------------------------------------------------------+ //| Check notification settings | //+------------------------------------------------------------------+ //--- If the notification flag is not set, we check the notification settings in the terminal and, if activated, we report this if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Now MetaQuotes ID is specified and sending notifications is allowed"); SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed"); ExtNotify=true; } //--- If the notification flag is set, but the terminal does not have permission for them, we report this if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))) { string caption=MQLInfoString(MQL_PROGRAM_NAME); string message="The terminal has a limitation on sending notifications. Please check your notification settings"; MessageBox(message, caption, MB_OK|MB_ICONWARNING); ExtNotify=false; } //+------------------------------------------------------------------+ //| Change account | //+------------------------------------------------------------------+ //--- If the current login is not equal to the previous one if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev) { //--- if we failed to wait for the account data to be updated, repeat on the next loop iteration if(!DataUpdateWait(balance_prev)) continue; //--- Received new account data //--- Save the current login and balance as previous ones for the next check account_prev=AccountInfoInteger(ACCOUNT_LOGIN); balance_prev=AccountInfoDouble(ACCOUNT_BALANCE); //--- Reset the sent message flag and call the account change handler Sent=false; AccountChangeHandler(); } //+------------------------------------------------------------------+ //| Daily reports | //+------------------------------------------------------------------+ //--- Fill the structure with data about local time and date MqlDateTime tm={}; TimeLocal(tm); //--- Clear the list of messages sent to MQID MessageList.Clear(); //--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day if(tm.day_of_year!=day_of_year_prev) { //--- If hours/minutes have reached the specified values for sending statistics if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin) { //--- If sending daily statistics is allowed if(InpSendDReport) { //--- update the lists of closed positions for the day on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); //--- if the settings are set to receive statistics from all accounts - //--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); //--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); //--- Create messages about trading statistics for a daily time range, //--- print the generated messages to the log and send them to MQID SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of days, //--- Create messages about trade statistics for the number of days in InpSendSReportDaysN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportDays) SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of months, //--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportMonths) SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of years, //--- Create messages about trade statistics for the number of years in InpSendSReportYearN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportYears) SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList); } //--- Set the current day as the previous one for subsequent verification day_of_year_prev=tm.day_of_year; } } //+------------------------------------------------------------------+ //| Weekly reports | //+------------------------------------------------------------------+ //--- If the day of the week is equal to the one set in the settings, if(tm.day_of_week==InpSendWReportDayWeek) { //--- if the message has not been sent yet and it is time to send messages if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin) { //--- update the lists of closed positions on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); //--- if the settings are set to receive statistics from all accounts - //--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); //--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); //--- If the settings allow sending trading statistics for a week, //--- Create messages about trading statistics from the beginning of the current week, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendWReport) SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for a month, //--- Create messages about trading statistics from the beginning of the current month, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendMReport) SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for a year, //--- Create messages about trading statistics from the beginning of the current year, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendYReport) SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the entire period, //--- Create messages about trading statistics from the start of the epoch (01.01.1970 00:00), //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendAReport) SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- Set the flag that all messages with statistics are sent to the journal Sent=true; } } //--- If the day of the week specified in the settings for sending statistics has not yet arrived, reset the flag of sent messages else Sent=false; //--- If the list of messages to send to MQID is not empty, call the function for sending notifications to a smartphone if(MessageList.Total()>0) SendMessage(MessageList); } //+------------------------------------------------------------------+ //| Service shutdown | //+------------------------------------------------------------------+ //--- Clear and delete lists of messages, symbols and magic numbers if(MessageList!=NULL) { MessageList.Clear(); delete MessageList; } if(SymbolsList!=NULL) { SymbolsList.Clear(); delete SymbolsList; } if(MagicsList!=NULL) { MagicsList.Clear(); delete MagicsList; } }
我们可以看到,当服务启动时,会检查终端是否有向智能手机发送通知的权限。调用CheckMQID()函数,该函数会检查每一项设置,并在客户端终端设置中请求启用所需的参数:
//+------------------------------------------------------------------+ //| Check for the presence of MetaQuotes ID | //| and permission to send notifications to the mobile terminal | //+------------------------------------------------------------------+ bool CheckMQID(void) { string caption=MQLInfoString(MQL_PROGRAM_NAME); // Message box header string message=caption+"-Service OK"; // Message box text int mb_id=IDOK; // MessageBox() return code //--- If MQID is not installed in the terminal settings, we will make a request to install it with explanations on the procedure if(!TerminalInfoInteger(TERMINAL_MQID)) { message="The client terminal does not have a MetaQuotes ID for sending Push notifications.\n"+ "1. Install the mobile version of the MetaTrader 5 terminal from the App Store or Google Play.\n"+ "2. Go to the \"Messages\" section of your mobile terminal.\n"+ "3. Click \"MQID\".\n"+ "4. In the client terminal, in the \"Tools - Settings\" menu, in the \"Notifications\" tab, in the MetaQuotes ID field, enter the received code."; mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONWARNING); } //--- If the Cancel button is pressed, inform about the refusal to use Push notifications if(mb_id==IDCANCEL) { message="You refused to enter your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal"; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } //--- If the Retry button is pressed, else { //--- If the terminal has MetaQuotes ID installed for sending Push notifications if(TerminalInfoInteger(TERMINAL_MQID)) { //--- if the terminal does not have permission to send notifications to a smartphone, if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { //--- show the message asking for permission to send notifications in the settings message="Please enable sending Push notifications in the terminal settings in the \"Notifications\" tab in the \"Tools - Settings\" menu."; mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONEXCLAMATION); //--- If the Cancel button is pressed in response to the message, if(mb_id==IDCANCEL) { //--- inform about the refusal to send notifications to a smartphone string message="You have opted out of sending Push notifications. The service will send notifications to the “Experts” tab of the terminal."; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } //--- If the Retry button is pressed in response to the message (this is expected to be done after enabling permission in the settings), //--- but there is still no permission to send notifications in the terminal, if(mb_id==IDRETRY && !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { //--- inform that the user has refused to send notifications to a smartphone, and messages will only be in the journal string message="You have not allowed push notifications. The service will send notifications to the “Experts” tab of the terminal."; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } } } //--- If the terminal has MetaQuotes ID installed for sending Push notifications, else { //--- inform that the terminal does not have MetaQuotes ID installed to send notifications to a smartphone, and messages will only be sent to the journal string message="You have not set your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal"; MessageBox(message, caption, MB_OK|MB_ICONINFORMATION); } } //--- Return the flag that MetaQuotes ID is set in the terminal and sending notifications is allowed return(TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)); }
在上述函数执行之后,会启动一个循环,在该循环中,会检查程序中发送通知的权限标志以及终端中这些权限的设置:
//+------------------------------------------------------------------+ //| Check notification settings | //+------------------------------------------------------------------+ //--- If the notification flag is not set, we check the notification settings in the terminal and, if activated, we report this if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { Print("Now MetaQuotes ID is specified and sending notifications is allowed"); SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed"); ExtNotify=true; } //--- If the notification flag is set, but the terminal does not have permission for them, we report this if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))) { string caption=MQLInfoString(MQL_PROGRAM_NAME); string message="The terminal has a limitation on sending notifications. Please check your notification settings"; MessageBox(message, caption, MB_OK|MB_ICONWARNING); ExtNotify=false; }
如果终端中的设置发生变化,服务会发出必要的警告:如果之前已启用但后来被禁用了,服务会报告说发送通知受到了限制。相反,如果之前被禁用了,但用户在设置中激活了权限,服务会报告说现在一切正常,并向智能手机发送一条关于此情况的通知。
在循环的下一步中,是对账户变更的检查:
//+------------------------------------------------------------------+ //| Change account | //+------------------------------------------------------------------+ //--- If the current login is not equal to the previous one if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev) { //--- if we failed to wait for the account data to be updated, repeat on the next loop iteration if(!DataUpdateWait(balance_prev)) continue; //--- Received new account data //--- Save the current login and balance as previous ones for the next check account_prev=AccountInfoInteger(ACCOUNT_LOGIN); balance_prev=AccountInfoDouble(ACCOUNT_BALANCE); //--- Reset the sent message flag and call the account change handler Sent=false; AccountChangeHandler(); }
一旦登录信息发生变化,并且与之前记住的登录信息不同,就调用等待当前账户加载的函数:
//+------------------------------------------------------------------+ //| Waiting for account data update | //+------------------------------------------------------------------+ bool DataUpdateWait(double &balance_prev) { int attempts=0; // Number of attempts //--- Until the program stop flag is disabled and until the number of attempts is less than the number set in REFRESH_ATTEMPTS while(!IsStopped() && attempts<REFRESH_ATTEMPTS) { //--- If the balance of the current account differs from the balance of the previously saved balance value, //--- we assume that we were able to obtain the account data, return 'true' if(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE)-balance_prev, 8)!=0) return true; //--- Wait half a second for the next attempt, increase the number of attempts and //--- log a message about waiting for data to be received and the number of attempts Sleep(500); attempts++; PrintFormat("%s::%s: Waiting for account information to update. Attempt %d", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__, attempts); } //--- If failed to obtain the new account data after all attempts, //--- report this to the log, write an empty value to the "previous balance" and return 'false' PrintFormat("%s::%s: Could not wait for updated account data... Try again", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__); balance_prev=EMPTY_VALUE; return false; }
函数等待,直到不再从终端缓存中接收到账户余额数据。毕竟,新账户的余额可能与之前账户的余额不同。该函数会尝试指定次数,以获得之前账户记住的余额与新账户余额之间的差异。如果失败(或者余额仍然相等),该函数最终会将之前账户的余额设置为EMPTY_VALUE,在下一次循环迭代中,将通过将这个新值与当前新账户的余额进行比较来检查是否接收到了新账户的当前数据,而这个新值很可能已经不再是账户余额了。
在循环的下一步中,安排了日期和时间的检查,以创建每日和每周的报告:
//+------------------------------------------------------------------+ //| Daily reports | //+------------------------------------------------------------------+ //--- Fill the structure with data about local time and date MqlDateTime tm={}; TimeLocal(tm); //--- Clear the list of messages sent to MQID MessageList.Clear(); //--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day if(tm.day_of_year!=day_of_year_prev) { //--- If hours/minutes have reached the specified values for sending statistics if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin) { //--- If sending daily statistics is allowed if(InpSendDReport) { //--- update the lists of closed positions for the day on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); //--- if the settings are set to receive statistics from all accounts - //--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); //--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); //--- Create messages about trading statistics for a daily time range, //--- print the generated messages to the log and send them to MQID SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of days, //--- Create messages about trade statistics for the number of days in InpSendSReportDaysN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportDays) SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of months, //--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportMonths) SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the specified number of years, //--- Create messages about trade statistics for the number of years in InpSendSReportYearN, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendSReportYears) SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList); } //--- Set the current day as the previous one for subsequent verification day_of_year_prev=tm.day_of_year; } } //+------------------------------------------------------------------+ //| Weekly reports | //+------------------------------------------------------------------+ //--- If the day of the week is equal to the one set in the settings, if(tm.day_of_week==InpSendWReportDayWeek) { //--- if the message has not been sent yet and it is time to send messages if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin) { //--- update the lists of closed positions on the current account ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); //--- if the settings are set to receive statistics from all accounts - //--- get a list of closed positions of all accounts that were active when the service was running if(InpUsedAccounts==USED_ACCOUNTS_ALL) PositionsList=ExtAccounts.GetCommonPositionsList(); //--- otherwise, get the list of closed positions of the current account only else PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer); //--- If the settings allow sending trading statistics for a week, //--- Create messages about trading statistics from the beginning of the current week, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendWReport) SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for a month, //--- Create messages about trading statistics from the beginning of the current month, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendMReport) SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for a year, //--- Create messages about trading statistics from the beginning of the current year, //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendYReport) SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- If the settings allow sending trading statistics for the entire period, //--- Create messages about trading statistics from the start of the epoch (01.01.1970 00:00), //--- print the created messages to the log and add them to the list for sending to MQID if(InpSendAReport) SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList); //--- Set the flag that all messages with statistics are sent to the journal Sent=true; } } //--- If the day of the week specified in the settings for sending statistics has not yet arrived, reset the flag of sent messages else Sent=false; //--- If the list of messages to send to MQID is not empty, call the function for sending notifications to a smartphone if(MessageList.Total()>0) SendMessage(MessageList);
在这里,代码列表中对所有逻辑都进行了注释。请注意,在循环中创建消息后,我们不能立即将其发送到智能手机。由于可能会有很多这样的消息(取决于设置中选择了哪些报告),并且对推送通知设置了严格的限制:每秒不超过两条消息,每分钟不超过十条消息。因此,所有创建的消息都被设置到标准库的CArrayString列表中。在所有报告都创建完成后,如果此列表不为空,则调用向智能手机发送通知的函数。在这个函数中,安排了所有必要的发送延迟,以确保不违反已建立的限制。
让我们来看看用于操作服务应用程序的所有函数。
返回一个具有指定范围统计信息的列表的函数:
//+------------------------------------------------------------------+ //| Return a list with the specified statistics range | //+------------------------------------------------------------------+ CArrayObj *GetListDataRange(ENUM_REPORT_RANGE range, CArrayObj *list, datetime &time_start, const int num_periods) { //--- Current date CDateTime current={}; current.Date(TimeLocal()); //--- Period start date CDateTime begin_range=current; //--- Set the period start time to 00:00:00 begin_range.Hour(0); begin_range.Min(0); begin_range.Sec(0); //--- Adjust the start date of the period depending on the specified period of required statistics switch(range) { //--- Day case REPORT_RANGE_DAILY : // decrease Day by 1 begin_range.DayDec(1); break; //--- Since the beginning of the week case REPORT_RANGE_WEEK_BEGIN : // decrease Day by (number of days passed in the week)-1 begin_range.DayDec(begin_range.day_of_week==SUNDAY ? 6 : begin_range.day_of_week-1); break; //--- Since the beginning of the month case REPORT_RANGE_MONTH_BEGIN : // set the first day of the month as Day begin_range.Day(1); break; //--- Since the beginning of the year case REPORT_RANGE_YEAR_BEGIN : // set Month to the first month of the year, and Day to the first day of the month begin_range.Mon(1); begin_range.Day(1); break; //--- Number of days case REPORT_RANGE_NUM_DAYS : // Decrease Day by the specified number of days begin_range.DayDec(fabs(num_periods)); break; //--- Number of months case REPORT_RANGE_NUM_MONTHS : // Decrease Month by the specified number of months begin_range.MonDec(fabs(num_periods)); break; //--- Number of years case REPORT_RANGE_NUM_YEARS : // Decrease Year by the specified number of years begin_range.YearDec(fabs(num_periods)); break; //---REPORT_RANGE_ALL Entire period default : // Set the date to 1970.01.01 begin_range.Year(1970); begin_range.Mon(1); begin_range.Day(1); break; } //--- Write the start date of the period and return the pointer to the list of positions, //--- the opening time of which is greater than or equal to the start time of the requested period time_start=begin_range.DateTime(); return CSelect::ByPositionProperty(list,POSITION_PROP_TIME,time_start,EQUAL_OR_MORE); }
该函数接收我们处理的统计信息范围(日、本周初、月初、年初、指定天数、月数、年数或整个交易期)的指示,以及需要按周期开始日期排序的已平仓头寸列表。接下来,根据接收到的统计信息范围,我们调整所需范围的起始日期,从计算出的日期开始获取并返回已平仓头寸的列表。
账户变更处理程序:
//+------------------------------------------------------------------+ //| Account change handler | //+------------------------------------------------------------------+ void AccountChangeHandler(void) { //--- Set the current account login and server long login = AccountInfoInteger(ACCOUNT_LOGIN); string server = AccountInfoString(ACCOUNT_SERVER); //--- Get the pointer to the account object based on the current account data CAccount *account = ExtAccounts.Get(login, server); //--- If the object is empty, create a new account object and get a pointer to it if(account==NULL && ExtAccounts.Create(login, server)) account=ExtAccounts.Get(login, server); //--- If the account object is eventually not received, report this and leave if(account==NULL) { PrintFormat("Error getting access to account object: %I64d (%s)", login, server); return; } //--- Set the current login and server values from the account object data ExtLogin =account.Login(); ExtServer=account.Server(); //--- Display the account data in the journal and display a message about the start of creating the list of closed positions account.Print(); Print("Beginning to create a list of closed positions..."); //--- Create the list of closed positions and report the number of created positions and the time spent in the journal upon completion ulong start=GetTickCount(); ExtAccounts.PositionsRefresh(ExtLogin, ExtServer); PrintFormat("A list of %d positions was created in %I64u ms", account.PositionsTotal(), GetTickCount()-start); }
在处理程序中,如果之前未使用过,则创建一个新的账户对象;如果之前已经连接过,则获取之前创建的账户的指针。然后创建账户已平仓头寸的列表。日志会显示关于开始创建历史头寸列表、完成创建以及花费的毫秒数的消息。
为指定时间范围创建统计信息的函数:
//+------------------------------------------------------------------+ //| Create statistics for the specified time range | //+------------------------------------------------------------------+ void SendReport(ENUM_REPORT_RANGE range, int num_periods, CArrayObj *list_common, CArrayString *list_symbols, CArrayLong *list_magics, CArrayString *list_msg) { string array_msg[2] = {NULL, NULL}; // Array of messages (0) for displaying in the journal and (1) for sending to a smartphone datetime time_start = 0; // Here we will store the start time of the statistics period CArrayObj *list_tmp = NULL; // Temporary list for sorting by symbols and magic number //--- Get a list of positions for the 'range' period CArrayObj *list_range=GetListDataRange(range, list_common, time_start, num_periods); if(list_range==NULL) return; //--- If the list of positions is empty, report to the journal that there were no transactions for the given period of time if(list_range.Total()==0) { PrintFormat("\"%s\" no trades",ReportRangeDescription(range, num_periods)); return; } //--- Create the lists of symbols and magic numbers of positions in the received list of closed positions for a period of time, while resetting them beforehand list_symbols.Clear(); list_magics.Clear(); CreateSymbolMagicLists(list_range, list_symbols, list_magics); //--- Create statistics on closed positions for the specified period, //--- print the generated statistics from array_msg[0] in the journal and //--- set the string from array_msg[1] to the list of messages for push notifications if(CreateStatisticsMessage(range, num_periods, REPORT_BY_RANGE, MQLInfoString(MQL_PROGRAM_NAME),time_start, list_range, list_symbols, list_magics, 0, array_msg)) { Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_RANGE, time_start)); // Statistics title Print(StatisticsTableHeader("Symbols ", InpCommissionsInclude, InpSpreadInclude)); // Table header Print(array_msg[0]); // Statistics for a period of time Print(""); // String indentation list_msg.Add(array_msg[1]); // Save the message for Push notifications to the list for later sending } //--- If statistics are allowed separately by symbols if(InpReportBySymbols) { //--- Display the statistics and table headers to the journal Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_SYMBOLS, time_start)); Print(StatisticsTableHeader("Symbol ", InpCommissionsInclude, InpSpreadInclude)); //--- In the loop by the list of symbols, for(int i=0; i<list_symbols.Total(); i++) { //--- get the name of the next symbol string symbol=list_symbols.At(i); if(symbol=="") continue; //--- sort out the list of positions leaving only positions with the received symbol list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_SYMBOL, symbol, EQUAL); //--- Create statistics on closed positions for the specified period by the current list symbol, //--- print the generated statistics from array_msg[0] and //--- set the string from array_msg[1] to the list of messages for push notifications if(CreateStatisticsMessage(range, num_periods, REPORT_BY_SYMBOLS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg)) { Print(array_msg[0]); list_msg.Add(array_msg[1]); } } //--- After the loop has completed for all symbols, display the separator line to the journal Print(""); } //--- If statistics are allowed separately by magic numbers if(InpReportByMagics) { //--- Display the statistics and table headers to the journal Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_MAGICS, time_start)); Print(StatisticsTableHeader("Magic ", InpCommissionsInclude, InpSpreadInclude)); //--- In the loop by the list of magic numbers, for(int i=0; i<list_magics.Total(); i++) { //--- get the next magic number long magic=list_magics.At(i); if(magic==LONG_MAX) continue; //--- sort out the list of positions leaving only positions with the received magic number list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_MAGIC, magic, EQUAL); //--- Create statistics on closed positions for the specified period by the current list magic number, //--- print the generated statistics from array_msg[0] and //--- set the string from array_msg[1] to the list of messages for push notifications if(CreateStatisticsMessage(range, num_periods, REPORT_BY_MAGICS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg)) { Print(array_msg[0]); list_msg.Add(array_msg[1]); } } //--- After the loop has completed for all magic numbers, display the separator line to the journal Print(""); } }
该函数调用为指定交易期间创建统计信息的函数,并在日志中显示表格标题和统计信息。推送通知的消息被设置在传递给该方法的消息列表的指针中。如果统计信息包括按交易品种和magic编号的报告,那么在将主要统计信息发送到日志之后,会显示按交易品种和magic编号的统计表格的标题和表头。随后是以表格形式显示的按交易品种和magic编号的报告。
创建并返回表格标题行的函数:
//+------------------------------------------------------------------+ //| Create and return the table header row | //+------------------------------------------------------------------+ string StatisticsTableHeader(const string first, const bool commissions, const bool spreads) { //--- Declare and initialize the table column headers string h_trades="Trades "; string h_long="Long "; string h_short="Short "; string h_profit="Profit "; string h_max="Max "; string h_min="Min "; string h_avg="Avg "; string h_costs="Costs "; //--- table columns disabled in the settings string h_commiss=(commissions ? "Commiss " : ""); string h_swap=(commissions ? "Swap " : ""); string h_fee=(commissions ? "Fee " : ""); string h_spread=(spreads ? "Spread " : ""); //--- width of table columns int w=TABLE_COLUMN_W; int c=(commissions ? TABLE_COLUMN_W : 0); //--- Table column separators that can be disabled in the settings string sep1=(commissions ? "|" : ""); string sep2=(spreads ? "|" : ""); //--- Create a table header row return StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s", w,first, w,h_trades, w,h_long, w,h_short, w,h_profit, w,h_max, w,h_min, w,h_avg, w,h_costs, c,h_commiss,sep1, c,h_swap,sep1, c,h_fee,sep1, w,h_spread,sep2); }
该函数创建以下行:
| Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread |
最后四列——它们的显示取决于在统计中是否允许使用佣金、隔夜利息(swap)、交易手续费和点差的值。
表头的第一列包含了通过参数传递给函数的名称,因为不同的表格应该有不同的表头。
要了解更多关于格式化文本消息的信息,可以阅读文章《研究PrintFormat()并应用现成示例》和《StringFormat()的现成示例和回顾》。
返回请求统计时间段描述表头的函数:
//+------------------------------------------------------------------+ //| Return the description header of the requested statistics period | //+------------------------------------------------------------------+ string StatisticsRangeTitle(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const datetime time_start, const string symbol=NULL, const long magic=LONG_MAX) { string report_by_str= ( report_by==REPORT_BY_SYMBOLS ? (symbol==NULL ? "by symbols " : "by "+symbol+" ") : report_by==REPORT_BY_MAGICS ? (magic==LONG_MAX ? "by magics " : "by magic #"+(string)magic+" ") : "" ); return StringFormat("Report %sfor the period \"%s\" from %s", report_by_str,ReportRangeDescription(range, num_periods), TimeToString(time_start, TIME_DATE)); }
根据统计范围和统计过滤器(按交易品种、magic编号或日期),会创建并返回以下类型的字符串:
Report for the period "3 months" from 2024.04.23 00:00
或者
Report by symbols for the period "3 months" from 2024.04.23 00:00
或者
Report by magics for the period "3 months" from 2024.04.23 00:00
等等。
返回包含统计信息的消息文本的函数:
//+------------------------------------------------------------------+ //| Return a message text with statistics | //+------------------------------------------------------------------+ bool CreateStatisticsMessage(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const string header, const datetime time_start, CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics, const int index, string &array_msg[]) { //--- Get a symbol and a magic number by index from the passed lists string symbol = list_symbols.At(index); long magic = list_magics.At(index); //--- If the passed lists are empty, or no data was received from them, return 'false' if(list==NULL || list.Total()==0 || (report_by==REPORT_BY_SYMBOLS && symbol=="") || (report_by==REPORT_BY_MAGICS && magic==LONG_MAX)) return false; CPosition *pos_min = NULL; // Pointer to the position with the minimum property value CPosition *pos_max = NULL; // Pointer to the position with the maximum property value CArrayObj *list_tmp = NULL; // Pointer to a temporary list for sorting by properties int index_min= WRONG_VALUE; // Index of the position in the list with the minimum property value int index_max= WRONG_VALUE; // Index of the position in the list with the maximum property value //--- Get the sum of the position properties from the list of positions double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT); // Total profit of positions in the list double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS); // Total commission of positions in the list double swap=PropertyValuesSum(list, POSITION_PROP_SWAP); // General swap of positions in the list double fee=PropertyValuesSum(list, POSITION_PROP_FEE); // Total deal fee in the list double costs=commissions+swap+fee; // All commissions double spreads=PositionsCloseSpreadCostSum(list); // Total spread costs for all items in the list //--- Define text descriptions of all received values string s_0=(report_by==REPORT_BY_SYMBOLS ? symbol : report_by==REPORT_BY_MAGICS ? (string)magic : (string)list_symbols.Total())+" "; string s_trades=StringFormat("%d ", list.Total()); string s_profit=StringFormat("%+.2f ", profit); string s_costs=StringFormat("%.2f ",costs); string s_commiss=(InpCommissionsInclude ? StringFormat("%.2f ",commissions) : ""); string s_swap=(InpCommissionsInclude ? StringFormat("%.2f ",swap) : ""); string s_fee=(InpCommissionsInclude ? StringFormat("%.2f ",fee) : ""); string s_spread=(InpSpreadInclude ? StringFormat("%.2f ",spreads) : ""); //--- Get the list of only long positions and create a description of their quantity list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_BUY, EQUAL); string s_long=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" "; //--- Get the list of only short positions and create a description of their quantity list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_SELL, EQUAL); string s_short=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" "; //--- Get the index of the position in the list with the maximum profit and create a description of the received value index_max=CSelect::FindPositionMax(list, POSITION_PROP_PROFIT); pos_max=list.At(index_max); double profit_max=(pos_max!=NULL ? pos_max.Profit() : EMPTY_VALUE); string s_max=(profit_max!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_max) : "No trades "); //--- Get the index of the position in the list with the minimum profit and create a description of the received value index_min=CSelect::FindPositionMin(list, POSITION_PROP_PROFIT); pos_min=list.At(index_min); double profit_min=(pos_min!=NULL ? pos_min.Profit() : EMPTY_VALUE); string s_min=(profit_min!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_min) : "No trades "); //--- Create a description of the average profit value of all positions in the list string s_avg=StringFormat("%.2f ", PropertyAverageValue(list, POSITION_PROP_PROFIT)); //--- Table column width int w=TABLE_COLUMN_W; int c=(InpCommissionsInclude ? TABLE_COLUMN_W : 0); //--- Separators for table columns that can be disabled in the settings string sep1=(InpCommissionsInclude ? "|" : ""); string sep2=(InpSpreadInclude ? "|" : ""); //--- For displaying in the journal, create a string with table columns featuring the values obtained above array_msg[0]=StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s", w,s_0, w,s_trades, w,s_long, w,s_short, w,s_profit, w,s_max, w,s_min, w,s_avg, w,s_costs, c,s_commiss,sep1, c,s_swap,sep1, c,s_fee,sep1, w,s_spread,sep2); //--- For sending MQID notifications, create a string with table columns featuring the values obtained above array_msg[1]=StringFormat("%s:\nTrades: %s Long: %s Short: %s\nProfit: %s Max: %s Min: %s Avg: %s\n%s%s%s%s%s", StatisticsRangeTitle(range, num_periods, report_by, time_start, (report_by==REPORT_BY_SYMBOLS ? symbol : NULL), (report_by==REPORT_BY_MAGICS ? magic : LONG_MAX)), s_trades, s_long, s_short, s_profit, s_max, s_min, s_avg, (costs!=0 ? "Costs: "+s_costs : ""), (InpCommissionsInclude && commissions!=0 ? " Commiss: "+s_commiss : ""), (InpCommissionsInclude && swap!=0 ? " Swap: "+s_swap : ""), (InpCommissionsInclude && fee!=0 ? " Fee: "+s_fee : ""), (InpSpreadInclude && spreads!=0 ? " Spreads: "+s_spread : "")); //--- All is successful return true; }
该函数利用我们之前实现的CSelect类来排序列表并搜索平仓仓位的索引。根据接收到的列表,会创建文本以在报告中显示数据。
报告文本在函数的最后部分创建,并且会生成两份——一份用于在日志中以表格形式显示,另一份用于推送通知的普通文本行。
从传递的列表中填充magic编号和仓位交易品种的列表的函数:
//+--------------------------------------------------------------------------+ //| Fill the lists of magic numbers and position symbols from the passed list| //+--------------------------------------------------------------------------+ void CreateSymbolMagicLists(CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics) { //--- If an invalid pointer to a list of positions is passed, or the list is empty, leave if(list==NULL || list.Total()==0) return; int index=WRONG_VALUE; // Index of the necessary symbol or magic number in the list //--- In a loop by the list of positions for(int i=0; i<list.Total(); i++) { //--- get the pointer to the next position CPosition *pos=list.At(i); if(pos==NULL) continue; //--- Get the position symbol string symbol=pos.Symbol(); //--- Set the sorted list flag for the symbol list and get the symbol index in the symbol list list_symbols.Sort(); index=list_symbols.Search(symbol); //--- If there is no such symbol in the list, add it if(index==WRONG_VALUE) list_symbols.Add(symbol); //--- Get the position magic number long magic=pos.Magic(); //--- Set the sorted list flag for the magic number list and get the magic number index in the list of magic numbers list_magics.Sort(); index=list_magics.Search(magic); //--- If there is no such magic number in the list, add it if(index==WRONG_VALUE) list_magics.Add(magic); } }
最初,我们不知道账户上使用了哪些交易品种和magic编号进行交易。为了按交易品种和magic编号接收报告,我们需要在所有已平仓仓位的完整列表中查找所有交易品种和所有magic编号,并将它们记录在相应的列表中。这个函数会接收到一个包含所有已平仓仓位的完整列表以及指向交易品种和magic编号列表的指针。所有找到的交易品种和magic编号都会被记录在对应的列表中。函数操作完成后,我们将得到两个填充好的交易品种和magic编号列表,这些列表随后可以用于分别编制按交易品种和magic编号的报告。
为了得到列表中所有仓位任何整数或实数属性的值之和,我们需要在循环中累加这个属性的值。我们为什么需要这个?例如,为了得到总点差的值,或者总盈亏的值。让我们编写一些函数,这些函数允许我们累加列表中所有仓位指定属性的值。
返回列表中所有项目指定整数属性值之和的函数:
//+------------------------------------------------------------------+ //| Return the sum of the values of the specified | //| integer property of all positions in the list | //+------------------------------------------------------------------+ long PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property) { long res=0; int total=list.Total(); for(int i=0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(property) : 0); } return res; }
在遍历传递给函数的列表时,我们使用循环索引从对象中获取指定属性的值,并将其加到结果值中。因此,在循环结束时,我们将得到传递给函数的列表中所有项目的指定属性值之和。
返回列表中所有项目指定实数属性值之和的函数:
//+------------------------------------------------------------------+ //| Return the sum of the values of the specified | //| real property of all positions in the list | //+------------------------------------------------------------------+ double PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property) { double res=0; int total=list.Total(); for(int i=0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(property) : 0); } return res; }
使用相同的原理,我们将创建返回指定属性平均值的函数。
返回列表中所有仓位指定整数属性的平均值的函数:
//+------------------------------------------------------------------+ //| Return the average value of the specified | //| integer property of all positions in the list | //+------------------------------------------------------------------+ double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property) { long res=0; int total=list.Total(); for(int i=0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(property) : 0); } return(total>0 ? (double)res/(double)total : 0); }
返回列表中所有仓位指定实数属性的平均值的函数:
//+------------------------------------------------------------------+ //| Return the average value of the specified | //| real property of all positions in the list | //+------------------------------------------------------------------+ double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property) { double res=0; int total=list.Total(); for(int i=0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.GetProperty(property) : 0); } return(total>0 ? res/(double)total : 0); }
返回列表中所有仓位平仓交易的点差成本总和的函数:
//+------------------------------------------------------------------+ //| Returns the sum of the spread costs | //| of deals closing all positions in the list | //+------------------------------------------------------------------+ double PositionsCloseSpreadCostSum(CArrayObj *list) { double res=0; if(list==NULL) return 0; int total=list.Total(); for(int i=0; i<total; i++) { CPosition *pos=list.At(i); res+=(pos!=NULL ? pos.SpreadOutCost() : 0); } return res; }
由于仓位对象本身没有“点差成本”这一属性,因此我们无法直接使用前面提到的函数。因此,在这里我们直接使用仓位对象的方法,该方法会在平仓时计算和返回点差成本。我们将列表中所有仓位得到的点差成本值累加到最终结果中,并返回这个累加值。
返回报告期描述的函数:
//+------------------------------------------------------------------+ //| Return the report period description | //+------------------------------------------------------------------+ string ReportRangeDescription(ENUM_REPORT_RANGE range, const int num_period) { switch(range) { //--- Day case REPORT_RANGE_DAILY : return("Daily"); //--- Since the beginning of the week case REPORT_RANGE_WEEK_BEGIN : return("Weekly"); //--- Since the beginning of the month case REPORT_RANGE_MONTH_BEGIN : return("Month-to-date"); //--- Since the beginning of the year case REPORT_RANGE_YEAR_BEGIN : return("Year-to-date"); //--- Number of days case REPORT_RANGE_NUM_DAYS : return StringFormat("%d days", num_period); //--- Number of months case REPORT_RANGE_NUM_MONTHS : return StringFormat("%d months", num_period); //--- Number of years case REPORT_RANGE_NUM_YEARS : return StringFormat("%d years", num_period); //--- Entire period case REPORT_RANGE_ALL : return("Entire period"); //--- any other default : return("Unknown period: "+(string)range); } }
根据传入的报告期值以及天数/月数/年数的数值,会创建一个描述性字符串并将其返回。
我们已经审视了服务程序的所有功能,以及程序本身——它的主循环。接下来,让我们编译并启动这个服务。编译完成后,程序将位于Navigator终端窗口的“服务”部分。
右键点击我们的服务并选择“添加服务”:
程序设置窗口被打开:
一旦服务被加载,将生成一份每日报告,包括
- 三个月的综合报告以及按交易品种和magic编号分类的三个月报告,
- 两年的综合报告以及按交易品种和magic编号分类的两年报告:
Reporter -Service notifications OK Reporter 68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging) Reporter Beginning to create a list of closed positions... Reporter A list of 155 positions was created in 8828 ms Reporter "Daily" no trades Reporter "7 days" no trades Reporter Report for the period "3 months" from 2024.04.23 00:00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | 2 | 77 | 17 | 60 | +247.00 | +36.70 | -0.40 | 3.20 | 0.00 | 0.00 | 0.00 | 0.00 | 5.10 | Reporter Reporter Report by symbols for the period "3 months" from 2024.04.23 00:00 Reporter | Symbol | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | EURUSD | 73 | 17 | 56 | +241.40 | +36.70 | -0.40 | 3.30 | 0.00 | 0.00 | 0.00 | 0.00 | 4.30 | Reporter | GBPUSD | 4 | 0 | 4 | +5.60 | +2.20 | +0.10 | 1.40 | 0.00 | 0.00 | 0.00 | 0.00 | 0.80 | Reporter Reporter Report by magics for the period "3 months" from 2024.04.23 00:00 Reporter | Magic | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | 0 | 75 | 15 | 60 | +246.60 | +36.70 | -0.40 | 3.28 | 0.00 | 0.00 | 0.00 | 0.00 | 4.90 | Reporter | 10879099 | 1 | 1 | 0 | +0.40 | +0.40 | +0.40 | 0.40 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 27394171 | 1 | 1 | 0 | +0.00 | +0.00 | +0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter Reporter Report for the period "2 years" from 2022.07.23 00:00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | 2 | 155 | 35 | 120 | +779.50 | +145.00 | -22.80 | 5.03 | 0.00 | 0.00 | 0.00 | 0.00 | 15.38 | Reporter Reporter Report by symbols for the period "2 years" from 2022.07.23 00:00 Reporter | Symbol | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | EURUSD | 138 | 30 | 108 | +612.40 | +36.70 | -22.80 | 4.43 | 0.00 | 0.00 | 0.00 | 0.00 | 6.90 | Reporter | GBPUSD | 17 | 5 | 12 | +167.10 | +145.00 | -7.20 | 9.83 | 0.00 | 0.00 | 0.00 | 0.00 | 8.48 | Reporter Reporter Report by magics for the period "2 years" from 2022.07.23 00:00 Reporter | Magic | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Commiss | Swap | Fee | Spread | Reporter | 0 | 131 | 31 | 100 | +569.10 | +36.70 | -8.50 | 4.34 | 0.00 | 0.00 | 0.00 | 0.00 | 8.18 | Reporter | 1 | 2 | 0 | 2 | +2.80 | +1.80 | +1.00 | 1.40 | 0.00 | 0.00 | 0.00 | 0.00 | 1.80 | Reporter | 123 | 2 | 0 | 2 | +0.80 | +0.40 | +0.40 | 0.40 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 1024 | 2 | 1 | 1 | +0.10 | +0.10 | +0.00 | 0.05 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | Reporter | 140578 | 1 | 0 | 1 | +145.00 | +145.00 | +145.00 | 145.00 | 0.00 | 0.00 | 0.00 | 0.00 | 4.00 | Reporter | 1114235 | 1 | 0 | 1 | +2.30 | +2.30 | +2.30 | 2.30 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 1769595 | 1 | 0 | 1 | +15.00 | +15.00 | +15.00 | 15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 1835131 | 1 | 0 | 1 | +3.60 | +3.60 | +3.60 | 3.60 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 2031739 | 1 | 0 | 1 | +15.00 | +15.00 | +15.00 | 15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 2293883 | 1 | 0 | 1 | +1.40 | +1.40 | +1.40 | 1.40 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 2949243 | 1 | 0 | 1 | -15.00 | -15.00 | -15.00 | -15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | Reporter | 10879099 | 1 | 1 | 0 | +0.40 | +0.40 | +0.40 | 0.40 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 12517499 | 1 | 1 | 0 | +15.00 | +15.00 | +15.00 | 15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | Reporter | 12976251 | 1 | 0 | 1 | +2.90 | +2.90 | +2.90 | 2.90 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 13566075 | 1 | 0 | 1 | +15.00 | +15.00 | +15.00 | 15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 13959291 | 1 | 0 | 1 | +15.10 | +15.10 | +15.10 | 15.10 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 15728763 | 1 | 0 | 1 | +11.70 | +11.70 | +11.70 | 11.70 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 16121979 | 1 | 0 | 1 | +15.00 | +15.00 | +15.00 | 15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 16318587 | 1 | 0 | 1 | -15.00 | -15.00 | -15.00 | -15.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | Reporter | 16580731 | 1 | 0 | 1 | +2.10 | +2.10 | +2.10 | 2.10 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter | 21299323 | 1 | 0 | 1 | -22.80 | -22.80 | -22.80 | -22.80 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | Reporter | 27394171 | 1 | 1 | 0 | +0.00 | +0.00 | +0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.10 | Reporter Reporter Beginning of sending 31 notifications to MQID Reporter 10 out of 31 messages sent. Reporter No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up. Reporter 20 out of 31 messages sent. Reporter No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up. Reporter 30 out of 31 messages sent. Reporter No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up. Reporter Sending 31 notifications completed
在向日志发送报告后,服务开始将报告发送到智能手机。共发送了31条消息,分4批发送,每批10条消息/分钟。
由于前一天或报告接收日期前七天没有交易,服务会提供相应的消息提示。
如果我们在设置中禁用按交易品种和magic编号的报告,同时禁用佣金和点差的报告,禁止指定天数内的报告,但允许当前周、月和年的日报,
统计信息如下所示:
Reporter -Service notifications OK Reporter 68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging) Reporter Beginning to create a list of closed positions... Reporter A list of 155 positions was created in 8515 ms Reporter "Daily" no trades Reporter "Weekly" no trades Reporter Report for the period "Month-to-date" from 2024.07.01 00:00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Reporter | 2 | 22 | 3 | 19 | +46.00 | +5.80 | -0.30 | 2.09 | 0.00 | Reporter Reporter Report for the period "Year-to-date" from 2024.01.01 00:00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Reporter | 2 | 107 | 31 | 76 | +264.00 | +36.70 | -7.20 | 2.47 | 0.00 | Reporter Reporter Report for the period "Entire period" from 1970.01.01 00:00 Reporter | Symbols | Trades | Long | Short | Profit | Max | Min | Avg | Costs | Reporter | 2 | 155 | 35 | 120 | +779.50 | +145.00 | -22.80 | 5.03 | 0.00 | Reporter Reporter Beginning of sending 3 notifications to MQID Reporter Sending 3 notifications completed
所有上述报告表都显示在终端的“专家”选项卡中。
报告发送到智能手机时,其形式会略有不同:
在这里,为了节省报告行的空间(其长度不能超过255个字符),任何零佣金值都不会在报告中显示(无论设置中是否允许显示)。
结论
我们以开发服务应用程序为例,探讨了存储各种数据以及根据各种标准获取数据列表的可能性。所探讨的概念使我们能够在对象列表中创建各种数据集,通过指定属性获取所需对象的指针,还可以根据所需属性创建排序后的对象列表。所有这些功能都允许我们以数据库的形式存储数据,并获取所需信息。我们可以将以交易报告等形式接收到的信息传递给日志,并通过MetaQuotes ID作为通知发送给用户的智能手机。
此外,我们还可以进一步改进今天所展示的程序,以扩展报告,并在单独的图表上以表格、图表和图示的形式展示这些报告,且完全按照用户所需的形式进行展示。这一切都得益于MQL5语言。
您可以解压所有项目文件和归档文件到MQL5终端文件夹中,并在事先编译Reporter.mq5文件后立即使用该程序。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15346