
Monitoring trading with push notifications — example of a MetaTrader 5 service
Contents
- Introduction
- Project Structure
- Deal class
- Historical position class
- Class for searching and sorting by properties of deals and positions
- Class collection of historical positions
- Account class
- Class collection of accounts
- Service app for creating trading reports and sending notifications
- Conclusion
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:
- structure of object properties "(Part I): Concept, data management and first results",
- structure of object lists "(Part II): Collection of historical orders and deals" and
- 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:
- 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.
- 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





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
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.
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?
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.