Русский 中文 Español Deutsch 日本語 Português
preview
Monitoring trading with push notifications — example of a MetaTrader 5 service

Monitoring trading with push notifications — example of a MetaTrader 5 service

MetaTrader 5Examples | 23 January 2025, 10:56
2 856 4
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

When trading on financial markets, an important component is the availability of information about the results of trades conducted over a certain period of time in the past.

Probably, every trader at least once faced the need to monitor the trading results for the past day, week, month, etc., in order to adjust their strategy based on the trading results. MetaTrader 5 client terminal provides good statistics in the form of reports allowing us evaluate the trading results in a convenient visual form. The report can help optimize our portfolio, as well as understand how to reduce risks and increase trading stability.

To analyze a strategy, click Report in the trading history context menu or Reports in the View menu (or simply press Alt+E):




Find more details in the article "New MetaTrader report: 5 most important trading metrics".

If for some reason the standard reports provided by the client terminal are not sufficient, the MQL5 language provides ample opportunities for creating your own programs, including the ones for generating reports and sending them to the trader’s smartphone. This is the possibility we will discuss today.

Our program should start at the moment of terminal launch, track the change of a trading account, daytime onset and the time for creating and sending reports. The Service program type will suit us for such purposes.

According to the MQL5 Reference, a Service is a program that, unlike indicators, EAs and scripts, does not require connection to a chart to operate. Like scripts, services do not handle any event except for trigger. To launch a service, its code should contain the OnStart handler function. Services do not accept any other events except Start, but they are able to send custom events to charts using EventChartCustom. Services are stored in <terminal_directory>\MQL5\Services.

Every service running in the terminal works in its own flow. This means that a looped service cannot affect the operation of other programs. Our service should work in an infinite loop, check for the specified time, read the entire trading history, create lists of closed positions, sort these lists by different criteria and display reports on them to the journal and in Push notifications to the user's smartphone. In addition, when the service is first launched or its settings are changed, the service should check the possibility of sending Push notifications from the terminal. To achieve this, we should arrange an interaction with the user via message windows waiting for the user's response and reaction. In addition, when sending Push notifications, there are limitations on the frequency of notifications per unit of time. Therefore, it is necessary to set delays in sending notifications. All this should not in any way affect the operation of other applications running in the client terminal. Based on all of the above, Services are the most convenient tool for creating such a project.

Now it is necessary to form an idea of the components necessary to assemble everything together.


Project Structure

Let's look at the program and its components from end to beginning:

  • Service app. The app has access to data from all accounts that have been active for the entire period of continuous operation of the service. From the accounts data, the app receives lists of closed positions and combines them into one general list. Depending on the settings, the service can use data on closed positions only from the current active account, or from the current and each of the previously used accounts in the client terminal.
    Trading statistics is created for the required trading periods based on the data on closed positions obtained from the list of accounts. Then it is sent to the user's smartphone as push notifications. Additionally, trading statistics is displayed in tabular form in the Experts terminal logs.
  • Collection of accounts. The collection includes a list of accounts the terminal was connected to during the continuous operation of the service. The accounts collection gives access to any account in the list and to all closed positions of all accounts. The lists are available in the service app, and the service makes selections and creates statistics based on them.
  • Account object class. Stores data for one account with a list (collection) of all closed positions, whose deals were carried out on this account during the continuous operation of the service. Provides access to account properties, to creating and updating the list of closed positions of this account and returns lists of closed positions by various selection criteria.
  • Class collection of historical positions. Contains a list of position objects, provides access to the properties of closed positions, to creating and updating the list of positions. Returns the list of closed positions.
  • Position object class. Stores and provides access to the properties of a closed position. Contains functionality for comparing two objects by various properties, which makes it possible to create lists of positions by various selection criteria. Contains a list of deals for this position and provides access to them.
  • Deal object class. Stores and provides access to the properties of a single deal. The object contains functionality for comparing two objects by various properties, which makes it possible to create lists of deals by various selection criteria.


We discussed the concept of recovering a closed position from the list of historical deals in the article "How to view deals directly on the chart without weltering in trading history". The list of deals allows determining the affiliaton of each deal with a certain position by position ID (PositionID) set in the deal properties. A position object is created, in which the found deals are placed into the list. Here we will do the same way. But in order to arrange the construction of deal and position objects, we will use a completely different, long-tested concept, where each object has identical methods of access to properties for setting and obtaining them. This concept allows us to create objects in a single common key, store them in lists, filter and sort by any object property and get new lists in the context of the specified property.

Read the following articles to properly understand the concept of building classes in this project:

  1. structure of object properties "(Part I): Concept, data management and first results"
  2. structure of object lists "(Part II): Collection of historical orders and deals" and
  3. methods for filtering objects in lists by properties "(Part III): Collection of market orders and positions, search and sorting"

In essence, the three articles describe the possibility of creating a database for any objects in MQL5, storing them in the database and obtaining the required properties and values. This is precisely the functionality that is needed in this project, and it is for this reason that it was decided to build objects and their collections according to the concept described in the articles. Only here it will be done a little simpler - without creating abstract object classes with protected constructors and without defining unsupported object properties in the classes. Everything will be simpler - each object will have its own list of properties, stored in three arrays with the ability to write and retrieve them. All these objects will be stored in the lists, where it will be possible to obtain new lists of only the required objects according to the specified properties.

In short, each object created in the project will have a set of its own properties, as, indeed, any object or entity in MQL5. Only in MQL5 there are standard functions for obtaining properties, and for project objects these will be methods for obtaining integer, real and string properties, set directly in the class of each object. Further on, all these objects will be stored in the lists — dynamic arrays of pointers to the CObject objects of the Standard Library. The Standard Library classes allow us to create complex projects with minimal costs. In this case, this means a database of closed positions of all accounts where trading was conducted, with the ability to obtain lists of objects sorted and selected by any required property.

    Any position exists only from the moment it is opened — performing In deal until the moment of closing — performing Out/OutBuy deal. In other words, it is an object that exists only as a market object. Any deal, on the contrary, is only a historical object, since a deal is simply the fact of execution of an order (trading order). Therefore, in the client terminal there are no positions in the historical list - they exist only in the list of current market positions.

    Accordingly, in order to recreate an already closed market position, it is necessary to "assemble" a previously existing position from historical deals. Fortunately, each deal features ID of a position the deal participated in. We need to go through the list of historical deals, get the next deal from the list, check the position ID and create the position object. Add the created deal object to the new historical position. We will implement this further. In the meantime, let's create the classes for the deal and position object we will continue to work with.


    Deal class

    In the terminal directory \MQL5\Services\, create the new AccountReporter\ folder featuring the Deal.mqh new file of the CDeal class.
    The class should be derived from the base class of the Standard Library CObject, while its file should be included into the newly created class:

    //+------------------------------------------------------------------+
    //|                                                         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
      {
      }
    
    


    Now let's add the enumerations of integer, real and string deal properties, while in the private, protected and public sections, declare the class member variables and the methods for handling deal properties:

    //+------------------------------------------------------------------+
    //|                                                         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();
      };
    
    

    Let's have a look at the implementation of the class methods.

    In the class constructor, consider that the deal has already been selected and we can get its properties:

    //+------------------------------------------------------------------+
    //| 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());
      }
    
    

    Save the deal properties, as well as Digits and Point of the symbol the deal was carried out for in the class properties arrays to perform calculations and display the deal information. Next, get the historical tick at the time of the deal. This way we provide access to the Bid and Ask prices at the time of the deal, and hence the ability to calculate the spread.


    The method that compares two objects by a specified property:

    //+------------------------------------------------------------------+
    //| 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);
         }
      }
    
    

    This is a virtual method that overrides the method of the same name in the CObject parent class. Depending on the comparison mode (one of the properties of the deal object), these properties are compared for the current object and for the one passed to the method by the pointer. The method returns 1 if the value of the current object property exceeds the one of the compared object. If less, we have -1. If the values are equal, we have 0.


    The method that returns a deal type description:

    //+------------------------------------------------------------------+
    //| 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();
         }
      }
    
    

    Depending on the deal type, its text description is returned. For this project, this method is redundant, since we will not use all types of deals, but only those that relate to the position - buy or sell.


    The method returning a description of the position change method:

    //+------------------------------------------------------------------+
    //| 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();
         }
      }
    
    


    The method returning a deal reason description:

    //+------------------------------------------------------------------+
    //| 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();
         }
      }
    
    


    The method returning a deal description:

    //+------------------------------------------------------------------+
    //| 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())));
      }
    
    


    The method that prints deal properties in the journal:

    //+------------------------------------------------------------------+
    //| Print deal properties in the journal                             |
    //+------------------------------------------------------------------+
    void CDeal::Print(void)
      {
       ::Print(this.Description());
      }
    
    


    The method returning time with milliseconds:

    //+------------------------------------------------------------------+
    //| 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'));
      }
    
    

    All methods that return and log text descriptions are intended to describe the deal. In this project, they are not actually needed, but one should always remember about expansion and improvements. For this reason such methods are present here.

    Method that receives the deal tick:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    The method logic is described in the code comments. After receiving a tick, the Ask and Bid prices are taken from it and the spread size is calculated as (Ask - Bid) / Point.

    If failed to obtain a tick using this method, obtain the average value of the spread using the method for obtaining the spread of the deal minute bar:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    The deal class is ready. The class objects will be stored in the list of deals in the historical position class, from which it will be possible to obtain pointers to the required deals and handle their data.


    Historical position class

    In \MQL5\Services\AccountReporter\, create the new file Position.mqh of the CPosition class.

    The class should be inherited from the CObject Standard Library base object class:

    //+------------------------------------------------------------------+
    //|                                                     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
      {
      }
    
    


    Since the position class will contain a list for this position, it is necessary to include to the created file the deal class file and the class file of the dynamic array of pointers to CObject objects:

    //+------------------------------------------------------------------+
    //|                                                     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
      {
      }
    
    


    Now let's add the enumerations of integer, real and string deal properties, while in the private, protected and public sections, declare the class member variables and the methods for handling position properties:

    //+------------------------------------------------------------------+
    //|                                                     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();
      };
    
    

    Let's have a look at the implementation of the class methods.

    Set the position ID and symbol from the parameters passed to the method in the class constructor, as well as write the account and symbol data:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    In the class destructor, clear the list of position deals:

    //+------------------------------------------------------------------+
    //| Destructor                                                       |
    //+------------------------------------------------------------------+
    CPosition::~CPosition()
      {
       this.m_list_deals.Clear();
      }
    
    


    The method that compares two objects by a specified property:

    //+------------------------------------------------------------------+
    //| 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;
         }
      }
    
    

    This is a virtual method that overrides the method of the same name in the CObject parent class. Depending on the comparison mode (one of the properties of the position object), these properties are compared for the current object and for the one passed to the method by the pointer. The method returns 1 if the value of the current object property exceeds the one of the compared object. If less, we have -1. If the values are equal, we have 0.


    The method that returns time with milliseconds:

    //+------------------------------------------------------------------+
    //| 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'));
      }
    
    


    The method returning the pointer to the open deal:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    In the loop via the list of position deals, look for a deal with the DEAL_ENTRY_IN (market entry) position change method and return the pointer to the found deal


    The method returning the pointer to the close deal:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    In the loop via the list of position deals, look for a deal with the DEAL_ENTRY_OUT (market exit) or DEAL_ENTRY_OUT_BY (close by) position change method and return the pointer to the found deal


    The method returning the open deal ticket:

    //+------------------------------------------------------------------+
    //| Return the open deal ticket                                      |
    //+------------------------------------------------------------------+
    ulong CPosition::DealIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    
    

    Get the pointer to the market entry deal and return its ticket.


    The method returning the close deal ticket:

    //+------------------------------------------------------------------+
    //| Return the close deal ticket                                     |
    //+------------------------------------------------------------------+
    ulong CPosition::DealOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    
    

    Get the pointer to the market exit deal and return its ticket.


    The method returning spread when opening:

    //+------------------------------------------------------------------+
    //| Return spread when opening                                       |
    //+------------------------------------------------------------------+
    int CPosition::SpreadIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    
    

    Get the pointer to the market entry deal and return the spread set in the deal.


    The method returning spread when closing:

    //+------------------------------------------------------------------+
    //| Return spread when closing                                       |
    //+------------------------------------------------------------------+
    int CPosition::SpreadOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    
    

    Get the pointer to the market exit deal and return the spread set in the deal.


    The method returning the Ask price when closing:

    //+------------------------------------------------------------------+
    //| Return Ask price when closing                                    |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutAsk(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ask() : 0);
      }
    
    

    Get the pointer to the market exit deal and return the Ask price value set in the deal.


    The method returning Bid price when closing:

    //+------------------------------------------------------------------+
    //| Return the Bid price when closing                                |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutBid(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Bid() : 0);
      }
    
    

    Get the pointer to the market exit deal and return the Bid price value set in the deal.


    The method returning a profit in points:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    The method returning the spread when closing:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    The method uses two methods for calculating the spread value:

    1. if the position profit is not equal to zero, then the cost of the spread is calculated in the proportion: spread size in points * position profit in money / position profit in points.
    2. if the position profit is zero, then the spread value is calculated using the equation: calculated tick value * spread size in points * deal volume.


    The method that sets the total commission for all deals:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    To determine the commission taken for the entire position lifetime, we need to add up the commissions of all deals in the position. In the loop through the list of position deals, add the commission of each deal to the resulting value, which is eventually returned from the method.


    The method setting the total deal fee:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    Here everything is exactly the same as in the previous method - we return the total sum of the Fee values of each position deal.

    Both of these methods must be called when all the position's trades have already been listed, otherwise the result will be incomplete.


    The method that adds a deal to the list of position deals:

    //+------------------------------------------------------------------+
    //| 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;   
      }
    
    

    The method logic is fully described in the code comments. The method receives the ticket of the currently selected deal. If there are no deals with such a ticket in the list yet, a new deal object is created and added to the list of position deals.


    The methods that return descriptions of some position properties:

    //+------------------------------------------------------------------+
    //| 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()));
      }
    
    

    These methods are used, for example, to display a position description in the journal.

    The Print method allows displaying the position description in the journal:

    //+------------------------------------------------------------------+
    //| 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();
         }
      }
    
    

    First, a header with a position description is printed. Then a description of each deal is printed using its Print() method in a loop through all the position deals.

    The historical position class is ready. Now let's create a static class for selecting, searching and sorting deals and positions by their properties.


    Class for searching and sorting by properties of deals and positions

    This class was thoroughly considered in the article "Library for easy and quick development of MetaTrader programs (Part III): Collection of market orders and positions, search and sorting" (Arranging the search section).

    In \MQL5\Services\AccountReporter\, create the new file Select.mqh of the CSelect class:

    //+------------------------------------------------------------------+
    //|                                                       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
      {
      }
    


    Set the enumeration of comparison modes, include the files of deal and position classes and declare the storage list:

    //+------------------------------------------------------------------+
    //|                                                       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
      {
      }
    
    


    Write all the methods for selecting objects and creating lists that satisfy the search criteria:

    //+------------------------------------------------------------------+
    //|                                                       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;
      }
    
    

    See "Arranging the search" section of the above-mentioned article.

    Now we are all set to create a class to handle the list of historical positions.


    Class collection of historical positions

    In the \MQL5\Services\AccountReporter\ terminal folder, create a new file PositionsControl.mqh of the CPositionsControl class.
    The class should be inherited from the CObject base object of the Standard Library, while the historical position class and the search and filter class files should be included into the file being created:

    //+------------------------------------------------------------------+
    //|                                             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
      {
      }
    


    Let's declare private, protected and public class methods:

    //+------------------------------------------------------------------+
    //| 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();
      };
    
    


    Let's consider the implementations of the declared methods.

    In the class constructor, set the sorting flag for the list of historical positions by close time in milliseconds:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CPositionsControl::CPositionsControl(void)
      {
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
      }
    
    


    In the class destructor, destroy the list of historical positions:

    //+------------------------------------------------------------------+
    //| Destructor                                                       |
    //+------------------------------------------------------------------+
    CPositionsControl::~CPositionsControl()
      {
       this.m_list_pos.Shutdown();
      }
    
    


    The method that returns the pointer to the position object from the list by 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;
      }
    
    


    The method returning the market position flag:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    


    The method returning a position type by a deal type:

    //+------------------------------------------------------------------+
    //| 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;
         }
      }
    
    

    Depending on the deal type, return the corresponding position type.


    The method returning the reason for opening a position by deal type:

    //+------------------------------------------------------------------+
    //| 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;
         }
      }
    
    

    Depending on the deal reason, return the corresponding reason for opening the position.


    The method creating or updating the list of historical positions:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    In a loop through the list of deals in the terminal, we receive the next deal and check its position ID. If this is a market position, skip the deal. If such a position is not yet in the list of historical positions, create a new position object and place it in the list of historical positions. If there are no deals with the ticket of the selected deal in the historical position object yet, add the deal to the list of position object deals. At the end of the loop of creating historical position objects for each position, set a common commission and a deal fee for all position deals. The method is virtual, which allows us to create more optimal logic in the inherited class if updating the list of positions is required much more often than at least once a day.


    The method that prints out the properties of positions and their deals in the journal:

    //+------------------------------------------------------------------+
    //| 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();
         }
      }
    
    

    In case we need to control the list of created historical positions, this method allows us to display each position with its deals in the journal.

    The service app "remembers" all accounts that were connected to during the continuous service operation. In other words, if there were no terminal restarts, and there was a connection to different accounts and trading servers, then the program remembers these accounts, which, in turn, store the lists of all closed positions. Trading reports are displayed for closed positions that were present on each of the connected accounts. Or, if the settings require displaying reports only from the current account, then, the lists of closed positions are sorted by the current account login and server.

    Based on the above, it turns out that we need an account class that will store a management class for the list of closed positions traded on this account. In the service app, we will receive the required account, which, in turn, will be used to retrieve the list of closed positions.


    Account class

    In \MQL5\Services\AccountReporter\, create the new file Account.mqh of the CAccount class.
    The class should be inherited from the CObject base object of the Standard Library, while the historical positions collection class file should be included into the created file:

    //+------------------------------------------------------------------+
    //|                                                      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
      {
      }
    


    In the protected section of the class, declare the object of control of historical positions (class of the list of closed account positions) and the list of integer, real and string properties:

    //+------------------------------------------------------------------+
    //| 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:
    
    


    In the public section, set the methods for handling lists, the ones for setting and returning account object properties and others:

    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() {}
      };
    
    


    Let's consider the implementation of the declared methods.

    In the class constructor, set all the properties of the current account to the object:

    //+------------------------------------------------------------------+
    //| 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:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    The method compares two account objects by only two properties - login and server name. If the logins of the two compared objects are equal, then the equality of the server name is checked. If the servers are the same as well, then the two objects are equal. Otherwise, either 1 or -1 is returned depending on whether the value of the property being compared between the two objects is greater or lesser.


    The methods that return descriptions of some account object properties:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    These methods are used to create the account description in the Description method:

    //+------------------------------------------------------------------+
    //| 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()));
      }
    
    

    The method returns a string as

    68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    
    

    This string can be printed to the log using the Print() method of the class.

    Now we need to create a class that will store lists of all accounts that were connected to during the service app operation.


    Class collection of accounts

    In the \MT5\MQL5\Services\AccountReporter\ terminal folder, create a new file Accounts.mqh of the CAccounts class.
    The class should be inherited from the CObject base object of the Standard Library, while the account class file should be included in the created file:

    //+------------------------------------------------------------------+
    //|                                                     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
      {
      }
    


    Declare the methods for the class operation in the private, protected and public sections:

    //+------------------------------------------------------------------+
    //| 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();
      };
    
    


    Let's consider the implementation of the declared methods.

    In the class constructor, set the sorted list flag to the list of accounts:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CAccounts::CAccounts()
      {
       this.m_list.Sort();
      }
    
    


    Clear the list of accounts in the class destructor:

    //+------------------------------------------------------------------+
    //| Destructor                                                       |
    //+------------------------------------------------------------------+
    CAccounts::~CAccounts()
      {
       this.m_list.Clear();
      }
    
    


    Protected method that creates a new account object and adds it to the list:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    This is a protected method, which works as part of the public method that creates a new account object:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    The method that returns the pointer to the specified account object:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    The method that updates the lists of positions of the specified account:

    //+------------------------------------------------------------------+
    //| 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();
      }
    
    


    The method that combines the lists of account positions and returns a combined list:

    //+--------------------------------------------------------------------+
    //| 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;
      }
    
    


    The method that returns the list of positions for the specified account:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    Get the pointer to the account object by login and server and return the pointer to its list of historical positions or NULL if failed to obtain the account object.

    All methods of this class are described in detail in the comments. If something is still unclear, you can ask questions in the article discussion.

    All classes that form the basis of the service app are ready. Let's start implementing the program itself.


    Service app for creating trading reports and sending notifications

    Let's decide on how the program should work.

    When the service is launched, the presence of a MetaQuotes ID in the client terminal and permission to send push notifications to the smartphone are checked.

    These settings can be found in the Tools -- Options menu in the Notifications tab:


    If there is no value in the MetaQuotes ID field, or the Enable Push notifications checkbox is not checked, the service will display a window asking you to set these parameters. If you choose not to set these parameters, you should be given a warning that there is no MQID or that sending notifications to the smartphone is not allowed, and that all messages will be in the journal only. If we set all the parameters, the reports will be sent both to the smartphone and to the Experts terminal journal. In the main loop, the program will constantly check the status of the notification sending settings in the terminal. Therefore, if permission to send notifications was not set when launching the service, we can always enable it after the service program has been launched - it will see the changes and enable the corresponding flag.

    In the service settings, we will be able to select the message sending parameters and time periods we need to make reports for:

    • General report parameters
      • which accounts to use for reports: (all or the current one),
      • whether to create reports by symbols: (yes/no) — first a report is created, and then separate reports are created from it for each of the symbols involved in the trade,
      • whether to create reports by magic numbers: (yes/no) — a report is created, and then separate reports are created from it for each of the magic numbers used in trading,
      • whether commissions should be included in the report: (yes/no) — if enabled, the costs of commissions, swaps and fees for conducting deals will be displayed separately in addition to the total amount of all costs,
      • whether possible losses on spreads when closing positions should be included in the report: (yes/no) — if enabled, the sum of of all possible spread expenses when closing will be displayed separately;
    • Daily report settings
      • whether reports on the last 24 hours should be sent; this also applies to reports for specified time periods: (yes/no) — if enabled, then reports for the last 24 hours and for configurable trading time intervals (for the number of days, months, and years) will be sent daily at the specified time,
      • report sending hour: (default 8),
      • report sending minutes: (default 0);
    • Daily report settings for custom time periods
      • whether to send reports for the specified number of days: (yes/no) — if enabled, then reports for the specified number of days will be created daily at the time specified above; the number of days of the report is calculated by subtracting the specified number of days from the current date,
      • number of days for reports for the specified number of days: (default 7),
      • whether to send reports for the specified number of months: (yes/no) — if enabled, then reports for the specified number of months will be created daily at the time specified above; the number of months of the report is calculated by subtracting the specified number of months from the current date,
      • number of months for reports for the specified number of months: (default 3),
      • whether to send reports for the specified number of years: (yes/no) — if enabled, then reports for the specified number of years will be created daily at the time specified above; the number of years of the report is calculated by subtracting the specified number of years from the current date,
      • number of years for reports for the specified number of years: (default 2);
    • Weekly report settings for all other periods
      • day of the week for sending weekly reports: (default is Saturday) — when the specified day comes, the reports specified in the settings below will be created and sent,
      • report sending hour: (default 8),
      • report sending minutes: (default 0),
      • whether to send reports from the beginning of the current week: (yes/no) — if enabled, then a report from the beginning of the current week is created weekly on the specified day,
      • whether to send reports from the beginning of the current month: (yes/no) — if enabled, then a report from the beginning of the current month is created weekly on the specified day,
      • whether to send reports from the beginning of the current year: (yes/no) — if enabled, then a report from the beginning of the current year is created weekly on the specified day,
      • whether to send reports for the entire trading period: (yes/no) — if enabled, then a report for the entire trading period is created weekly on the specified day.


    These settings will be quite sufficient to cover most of the trading periods of interest for creating reports on them.

    In the \MQL5\Services\AccountReporter\ terminal folder, create the new file Reporter.mq5 of the service app:



    Let's enter the necessary macro substitutions, connect external files, write enumerations, inputs and global variables for the program to work:

    //+------------------------------------------------------------------+
    //|                                                     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()
      {
      }
    
    


    We can see the \MQL5\Include\Tools\DateTime.mqh file is included. This is a structure inherited from standard 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);
      };
    
    

    The structure contains ready-made methods for working with dates and time. We will need to calculate the start time of the statistics period. This is exactly where the structure methods come in handy in order not to independently calculate the validity of the dates obtained when subtracting the number of days, weeks, months and years from the current date. Here all calculations are performed with correction of incorrect values. For example, if more days are subtracted from the current date than there are in the month, then the resulting date should be adjusted by calculating the month and the day, while keeping leap years in mind. But it is easier to just take and use the methods of reducing days, months and years of a given structure to immediately obtain the correct final date.

    The service app itself should work in an infinite loop. In the loop, arrange a delay of about a second. After the end of the wait, all checks and calculations are to be arranged. The entire body of the loop is divided into titled blocks for clarity and better understanding. Let's look at the program body itself:

    //+------------------------------------------------------------------+
    //| 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;
         }
      }
    
    

    As we can see, when the service is launched, the presence of permissions in the terminal to send notifications to the smartphone is checked. The CheckMQID() function is called, where each of the settings is checked and requests are made to enable the required parameters in the client terminal settings:

    //+------------------------------------------------------------------+
    //| 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));
      }
    
    


    After the function presented above is executed, a loop is launched, where the flag for permission to send notifications in the program and the settings for these permissions in the terminal are controlled:

          //+------------------------------------------------------------------+
          //| 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;
            }
    
    

    If something changes in the terminal, the service issues the necessary warnings: if it was enabled and became disabled, the service will report that there are restrictions on sending notifications. If, on the contrary, it was disabled, but the user activated permissions in the settings, the service will report that everything is now normal and send a notification about this to a smartphone.

    Next in the loop there is a check for account change:

          //+------------------------------------------------------------------+
          //| 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();
            }
    
    

    As soon as the login changes and becomes different from the previously remembered one, call the function for waiting for the loading of the current account:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    The function waits until account balance data is no longer received from the terminal cache. After all, the balance on the new account is probably different from the one on the previous account. The function makes a specified number of attempts to obtain the difference between the remembered balance of the previous account and the one on the new one. In case of failure (or if the balances are still equal), the function will eventually set EMPTY_VALUE to the previous balance, and at the next iteration of the loop, there will be a check for receiving current data for the new account by comparing it with this new value, which most likely can no longer be on the account balance.

    Next in the loop, date and time checks are arranged to create daily and weekly reports:

          //+------------------------------------------------------------------+
          //| 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);
    
    

    Here all the logic is commented in the listing. Note that we cannot send a message immediately after it is created in the loop to send messages to a smartphone. Since there can be many such messages (depending on which reports are selected in the settings), and strict restrictions are set for Push notifications: no more than two messages per second and no more than ten messages per minute. Therefore, all created messages are set to the CArrayString list of the Standard Library. After all reports have been created, and if this array is not empty, call the function for sending notifications to a smartphone. In this function, all the necessary sending delays are arranged so as not to violate the established restrictions.

    Let's look at all the functions used to operate the service app.

    The function that returns a list with a specified range of statistics:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    The function receives an indication of the range of statistics we are working with (daily, from the beginning of the week, month, year, with a specified number of days, months, years, or the full trading period), as well as the list of closed positions that will need to be sorted by the start date of the period. Next, depending on the range of the received statistics, we adjust the starting date of the required range, get and return a list of closed positions from the beginning of the calculated date.


    Account change handler function:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    

    In the handler, a new account object is created if it has not been used before, or a pointer to a previously created account is obtained if there was a connection to it before. Then the list of the account closed positions is created. The journal displays messages about the start of creating a list of historical positions, its completion, and the number of milliseconds spent on it.


    The function that creates statistics for the specified time range:

    //+------------------------------------------------------------------+
    //| 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("");
         }
      }
    
    

    The function calls the function for creating statistics for the specified trading period and displays the table header and statistics in the journal. Messages for push notifications are set in the pointer to the list of messages passed to the method. If the statistics include reports by symbols and magic numbers, then after the main statistics are sent to the journal, the title and header of the statistics table by symbols and magic numbers is displayed. They are followed by a report by symbols and magic numbers in tabular form.


    The function that creates and returns the table header row:

    //+------------------------------------------------------------------+
    //| 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);
                           
      }
    
    

    The function creates the following row

    |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    
    

    The last four columns - their display depends on whether the use of commission, swap, deal fee and spread values is allowed in the statistics.

    The header first column contains the name passed to the function in the parameters, since different tables should have different headers.

    Find out more about formatting text messages in the articles "Studying PrintFormat() and applying ready-made examples" and "StringFormat(). Review and ready-made examples".


    The function that returns the description header of the requested statistics period:

    //+------------------------------------------------------------------+
    //| 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));
      }
    
    

    Depending on the statistics range and statistics filters (by symbol, magic number or date), a string of the following type is created and returned:

    Report for the period "3 months" from 2024.04.23 00:00
    
    

    or

    Report by symbols for the period "3 months" from 2024.04.23 00:00
    
    

    or

    Report by magics for the period "3 months" from 2024.04.23 00:00
    
    

    etc.

    The function returning a message text with statistics:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    The function uses sorting the list and searching for indices of closed positions using the CSelect class we implemented earlier. Texts are created for displaying data in the report from the received lists.
    Report texts are created at the very end of the function in two copies — for tabular display in the journal and for a regular line for a push notification.


    The function that fills the lists of magic numbers and position symbols from the passed list:

    //+--------------------------------------------------------------------------+
    //| 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);
         }
      }
    
    

    Initially, we do not know what symbols and magic numbers were used to trade on the account. In order to receive reports by symbols and magic numbers, it is necessary to find all symbols and all magic numbers of closed positions in the full list of all closed positions and write them down in the corresponding lists. A complete list of all closed positions and pointers to symbol and magic number lists are passed to this function. All found symbols and magic numbers are recorded in the corresponding lists. After the function operation is complete, we will have two filled lists of symbols and magic numbers, which can then be used to compile reports on symbols and magic numbers separately.

    To get the sum of the values of any integer or real property of all positions in the list, we need to add the values of this property in a loop. Why do we need this? For example, to get the value of the total spread, or the total profit or loss. Let's write the functions that allow us to add up the values of the specified properties of all positions in the list.

    The function that returns the sum of the values of a specified integer property of all items in the list:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    In a loop along the list whose pointer is passed to the function, get the value of the specified property from the object by the loop index and add it to the resulting value. As a result, at the end of the loop, we will have the sum of the values of the specified property of all positions in the list passed to the function.

    The function that returns the sum of the values of a specified real property of all items in the list:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    


    Using the same principle, we will create functions that return the average value of the specified property.

    The function returning the average value of a specified integer property of all positions in the list:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    The function returning the average value of the specified material property of all positions in the list:

    //+------------------------------------------------------------------+
    //| 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);
      }
    
    


    The function returning the sum of spread costs for deals closing all positions in the list:

    //+------------------------------------------------------------------+
    //| 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;
      }
    
    

    Since the position does not have the "spread cost" property, we cannot use the above functions here. Therefore, here we directly use the method of the position object, which calculates and returns the cost of the spread when closing a position. Add all the obtained values of all positions in the list to the final result and return the obtained value.


    The function returning the eport period description:

    //+------------------------------------------------------------------+
    //| 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);
         }
      }
    
    

    Depending on the passed value of the report period and the number of days/months/years, a description string is created and returned.


    We have looked at all the functions of the service program, and the program itself — its main loop. Let's compile it and launch the service. After compilation, the program will be located in the Services section of the Navigator terminal window.

    Right click on our service and select "Add service":


    The program settings window opens:



    Once the service is launched, a daily report will be created, including

    • general report for three months and report for three months in terms of symbols and magic numbers,
    • general report for two years and report for two years in terms of symbols and magic numbers:
    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
    
    

    After sending reports to the journal, the service starts sending reports to the smartphone. 31 messages were sent in 4 batches - 10 messages per minute.

    Since there was no trading the previous day or for seven days prior to the date of the report receipt, the service provides the appropriate message.

    If we disable reports by symbols and magic numbers in the settings, as well as disable commissions and spreads, prohibit reports for a specified number of days, but allow daily reports for the current week, month and year,


    The statistics look as follows:

    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
    
    

    All the above report tables are displayed in the terminal Experts tab.

    The reports are sent to the smartphone in a slightly different form:


    Here, any zero commission values are not displayed in the report (regardless of whether they are allowed in the settings) to save space in the report line, the length of which cannot exceed 255 characters.


    Conclusion

    We have considered the possibility of storing various data and obtaining lists of data according to all sorts of criteria using the development of a service app as an example. The considered concept allows us to create sets of various data in lists of objects, obtain pointers to the required objects by the specified properties and also create lists of objects sorted by the required property. All this allows storing data in the form of a database and obtain the necessary information. We can present the information we receive in the form of, say, trading reports, pass them to a journal and send them as notifications to the user’s smartphone via MetaQuotes ID.

    It is also possible to go further and refine the program presented today to expand reports and display them on a separate chart in the form of tables, charts and diagrams, and exactly in the form that the user requires. All this is made possible by the MQL5 language.

    You can unzip all the project files and the archive into the MQL5 terminal folder and immediately use the program, while compiling the Reporter.mq5 file beforehand.


    Translated from Russian by MetaQuotes Ltd.
    Original article: https://www.mql5.com/ru/articles/15346

    Attached files |
    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)
    Last comments | Go to discussion (4)
    theonementor
    theonementor | 31 Jul 2024 at 14:29
    correct me if I'm wrong, why are you re-doing yourself? Account, Position and Select classes, looks awfully similar to what you already have in do easy library! why re-implement what you already have ready? If you needed additional functionality, wouldn't it be better to add the functionality to already existing library and use it?
    Artyom Trishkin
    Artyom Trishkin | 31 Jul 2024 at 14:50
    theonementor # :
    Correct me if I'm wrong, why are you remodelling yourself? The Account, Position and Select classes are very similar to what you already have in your library do easy! why re-implement something that is already ready? If you need additional functionality, wouldn't it be better to add it to an existing library and use it?

    This article is not related to library articles. But the concept of building objects is taken from the library. Please re-read the article for understanding.

    theonementor
    theonementor | 1 Aug 2024 at 12:38
    Artyom Trishkin # :

    This article is not related to the library articles. But the concept of constructing objects is taken from the library. Please reread the article to understand.

    will you add this functionality to library later?
    Artyom Trishkin
    Artyom Trishkin | 1 Aug 2024 at 12:45
    theonementor #:
    will you add this functionality to library later?

    Later, yes.

    But it will not be a repetition of what is written here. It will be possible to design something for yourself from a set of methods.

    From Basic to Intermediate: Variables (I) From Basic to Intermediate: Variables (I)
    Many beginning programmers have a hard time understanding why their code doesn't work as they expect. There are many things that make code truly functional. It's not just a bunch of different functions and operations that make the code work. Today I invite you to learn how to properly create real code, rather than copy and paste fragments of it. The materials presented here are for didactic purposes only. Under no circumstances should the application be viewed for any purpose other than to learn and master the concepts presented.
    Master MQL5 from Beginner to Pro (Part III): Complex Data Types and Include Files Master MQL5 from Beginner to Pro (Part III): Complex Data Types and Include Files
    This is the third article in a series describing the main aspects of MQL5 programming. This article covers complex data types that were not discussed in the previous article. These include structures, unions, classes, and the 'function' data type. It also explains how to add modularity to your program using the #include preprocessor directive.
    Price Action Analysis Toolkit Development (Part 9): External Flow Price Action Analysis Toolkit Development (Part 9): External Flow
    This article explores a new dimension of analysis using external libraries specifically designed for advanced analytics. These libraries, like pandas, provide powerful tools for processing and interpreting complex data, enabling traders to gain more profound insights into market dynamics. By integrating such technologies, we can bridge the gap between raw data and actionable strategies. Join us as we lay the foundation for this innovative approach and unlock the potential of combining technology with trading expertise.
    MQL5 Trading Toolkit (Part 7): Expanding the History Management EX5 Library with the Last Canceled Pending Order Functions MQL5 Trading Toolkit (Part 7): Expanding the History Management EX5 Library with the Last Canceled Pending Order Functions
    Learn how to complete the creation of the final module in the History Manager EX5 library, focusing on the functions responsible for handling the most recently canceled pending order. This will provide you with the tools to efficiently retrieve and store key details related to canceled pending orders with MQL5.