English Русский
preview
通过推送通知监控交易——一个MetaTrader 5服务的示例

通过推送通知监控交易——一个MetaTrader 5服务的示例

MetaTrader 5示例 | 3 二月 2025, 15:25
62 0
Artyom Trishkin
Artyom Trishkin

内容


引言

当在金融市场上进行交易时,一个关键要素是能够获取关于过去某一段时间内交易结果的信息。

或许,每位交易者都至少有过一次需要监控过去一天、一周、一个月等时间段内的交易结果,以便根据这些交易结果来调整自己的交易策略的经历。MetaTrader 5客户端提供了以报告形式展现的良好统计数据,使我们能够以直观便捷的方式评估交易结果。这份报告有助于我们优化投资组合,同时也能帮助我们理解如何降低风险并提高交易的稳定性。

要分析策略,请在交易历史上下文菜单中点击“报告”,或在“查看”菜单中点击“报告”(或直接按Alt+E):




在文章《MetaTrader新版报告:五大最重要交易指标》中可以找到更多详细信息。

倘若因某些缘由,客户端终端所提供的标准报告未能满足您的需求,MQL5语言则提供了丰富的可能性,让您能够创建自己的程序,包括那些用于生成报告并将其发送至交易者智能手机的程序。这就是我们今天要讨论的内容。

我们的程序应该在终端启动时开始运行,跟踪交易账户的变化、白天的开始时间以及创建和发送报告的时间。对于这种用途,服务类型的程序将非常适合我们。

根据MQL5参考手册,服务(Service)是一种程序,与指标、智能交易系统(EAs)和脚本不同,它无需连接到图表即可运行。与脚本类似,服务除了触发事件外,不处理任何其他事件。若要启动服务,其代码应包含OnStart处理程序函数。服务除了启动事件(Start)外,不接受任何其他事件,但它们能够使用EventChartCustom向图表发送自定义事件。服务存储在<terminal_directory>\MQL5\Services目录中。

每一个在终端中运行的服务都在其自身的流程中运作。这意味着一个循环运行的服务不会影响其他程序的运作。我们的服务应当在一个无限循环中工作,检查指定的时间,读取整个交易历史,创建已平仓头寸的列表,根据不同的标准对这些列表进行排序,并将报告显示在日志中,并通过推送通知发送到用户的智能手机上。此外,当服务首次启动或其设置发生更改时,服务应检查从终端发送推送通知的可能性。为了实现这一点,我们应当通过消息窗口与用户进行交互,等待用户的响应和反应。同时,在发送推送通知时,对单位时间内通知的频率存在限制。因此,有必要在发送通知时设置延迟。所有这些都不应以任何方式影响客户端终端中运行的其他应用程序的操作。基于以上所有考虑,服务是创建此类项目最方便的工具。

现在有必要形成一个将所有这些组件组装在一起的方案。


项目结构

让我们从后往前审视这个程序及其组件:

  • 服务应用。该应用可以访问在服务连续运行期间所有活跃账户的数据。应用会从账户数据中接收已平仓头寸的列表,并将它们合并成一个总列表。根据设置,服务可以仅使用当前活跃账户中的已平仓头寸数据,或使用客户端终端中当前及之前使用过的每个账户的已平仓头寸数据。
    基于从账户列表中获取的已平仓头寸数据,为所需的交易时段创建交易统计信息。然后,它将以推送通知的形式发送到用户的智能手机。此外,交易统计信息还会以表格形式显示在终端的专家(Experts)日志中。
  • 账户集合。集合包含服务连续运行期间终端连接过的账户列表。账户集合可以访问列表中的任何账户以及所有账户的已平仓头寸。这些列表可在服务应用中查看,服务会根据它们进行选择并创建统计信息。
  • 账户对象类。存储一个账户的数据,包括该账户在服务连续运行期间所有已平仓头寸的列表(集合)。提供访问账户属性的功能,可以创建和更新该账户的已平仓头寸列表,并根据各种选择标准返回已平仓头寸列表。
  • 历史头寸集合类。包含头寸对象的列表,提供访问已平仓头寸属性的功能,可以创建和更新头寸列表。返回已平仓头寸列表。
  • 头寸对象类。存储并提供访问已平仓头寸属性的功能。包含通过各种属性比较两个对象的功能,从而可以根据各种选择标准创建头寸列表。包含该头寸的成交订单列表,并提供访问这些成交的功能。
  • 成交订单对象类。存储并提供访问单个成交订单属性的功能。对象包含通过各种属性比较两个对象的功能,从而可以根据各种选择标准创建成交订单列表。


在文章《如何在图表上直接查看成交记录而不必在交易历史中挣扎》中,我们讨论了从历史成交列表中恢复已平仓头寸的概念。成交订单列表允许通过其属性中设置的头寸ID(PositionID)来确定每笔交易与特定头寸的关联。创建一个头寸对象,将找到的成交订单放入其列表中。在这里,我们将采用同样的方式。但是,为了构建成交订单和头寸对象,我们将使用一个完全不同且经过长期测试的概念,其中每个对象都有相同的方法来设置和获取属性的功能。这个概念允许我们在一个共同的键中创建对象,将它们存储在列表中,根据任何对象属性进行过滤和排序,并根据指定的属性获取新的列表。

阅读以下文章以正确理解本项目中构建类的概念:

  1. 对象属性结构《(第一部分):概念、数据管理和初步结果》, 
  2. 对象列表结构《(第二部分):历史订单和成交的集合》以及
  3. 根据属性过滤列表中对象的方法《(第三部分):市价订单和头寸的集合,搜索和排序》

实质上,这三篇文章描述了在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);
      }
    
    

    该方法使用两种方法来计算点差值:

    1. 如果仓位利润不等于零,则按以下比例计算点差成本:点差点数 * 仓位利润(货币)/ 仓位利润(点数)
    2. 如果仓位利润为零,则使用以下公式计算点差值:计算出的每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

    附加的文件 |
    Deal.mqh (49.73 KB)
    Position.mqh (67.53 KB)
    Select.mqh (43.22 KB)
    Account.mqh (32.94 KB)
    Accounts.mqh (15.89 KB)
    Reporter.mq5 (101.21 KB)
    MQL5.zip (36.5 KB)
    如何将聪明资金概念(SMC)与 RSI 指标结合到 EA 中 如何将聪明资金概念(SMC)与 RSI 指标结合到 EA 中
    聪明资金概念(结构突破)与 RSI 指标相结合,可根据市场结构做出明智的自动交易决策。
    特征向量和特征值:MetaTrader 5 中的探索性数据分析 特征向量和特征值:MetaTrader 5 中的探索性数据分析
    在这篇文章中,我们将探索特征向量和特征值在探索性数据分析中的不同应用方式,以揭示数据中的独特关系。
    构建蜡烛图趋势约束模型(第7部分):为EA开发优化我们的模型 构建蜡烛图趋势约束模型(第7部分):为EA开发优化我们的模型
    在本文中,我们将详细探讨为开发专家顾问(EA)所准备的指标的相关内容。我们不仅会讨论如何对当前版本的指标进行进一步改进,以提升其准确性和功能,还会引入全新的功能来标记退出点,以弥补之前版本仅具备识别入场点功能的不足。
    改编版 MQL5 网格对冲 EA(第 IV 部分):优化简单网格策略(I) 改编版 MQL5 网格对冲 EA(第 IV 部分):优化简单网格策略(I)
    在第四篇中,我们重新审视了之前开发的“简单对冲”和“简单网格”智能系统(EA)。我们的专注点转移到通过数学分析和暴力方式完善简单网格 EA,旨在优化策略用法。本文深入策略的数学优化,为在以后文章中探索未来基于编码的优化奠定了基础。