
How to view deals directly on the chart without weltering in trading history
Contents
Introduction
Modern traders often face the problem of analyzing large amounts of data related to trading operations. In the MetaTrader 5 client terminal, the display of trading history can become ineffective and confusing due to the chart being overloaded with labels of open and closed positions. This is especially true for traders who work with a large number of deals, where the chart quickly fills up, making it almost impossible to analyze trading activity and make informed decisions.
The purpose of this article is to offer a solution that will make it easier to perceive and analyze trading history. We will develop a mechanism for step-by-step display of closed positions and improved deal information. This will allow traders to focus on each individual deal and gain a deeper understanding of their trading operations.
We will implement the functions that will allow us to:
- View closed positions one by one using the navigation keys.
- Improve tooltips by providing more detailed information about each deal.
- Center the chart so that the most important elements are always visible.
In addition, the data structure required to implement this functionality will be examined in detail, and the basic principles of accounting for deals and positions in MetaTrader 5 will be proposed. As a result, traders will be able to manage their trading history more effectively and make informed decisions based on the information they receive.
In the MetaTrader 5 client terminal, we can display the trading history by checking the "Show trade history" option on the Show tab of the chart settings (F8 key):
By checking the option, we allow the terminal to display the entire trading history on the chart in the form of labels for opening/closing positions connected by lines. In this case, all deals carried out on the symbol are displayed in full:
If there are a lot of deals, the chart gets cluttered with labels, making it difficult to see anything on it. The tooltips that appear when hovering over a deal label or a line connecting trades are not particularly informative:
The solution featured here will display only the last closed position when launched. Movement through the remaining positions will be carried out by pressing the cursor keys:
- the up key will display the first closed position;
- the down key will display the last closed position;
- the left key will display the previous closed position;
- the right arrow key will display the next closed position.
The tooltips of deals and connecting lines are to display more useful information. Besides, when holding down the Shift key, information on the currently selected closed position will be displayed on the chart:
When displaying each subsequent closed position, we will center the chart so that position opening/closing labels and the line connecting them are located in the center.
In case both opening and closing labels do not fit inside one chart screen, center the chart so that the opening deal is located on the second left visible bar of the chart.
Let's look at the basic principles of accounting for deals and positions, as well as the structure of the classes we are going to create here.
How is a position formed? First, a trade request is sent to the server - an order (trade order). The order can either be canceled or executed. When an order is executed, we receive a deal - the fact that the trade request has been executed. If there were no positions at the time the order was executed, the deal creates a position in a certain direction. If there was a position, then there are several options, depending on the position accounting type. In case of the netting accounting, only one position is possible on one symbol. Accordingly, the deal generated by the trade order modifies an existing position. It can be:
- closed — if a sell was performed for a long position with a volume equal to the position volume:
Position 1.0 Buy - Trade 1.0 Sell = volume 0 (closing a position); - partially closed — if a sell was performed for a long position with a volume less than the position volume:
Position 1.0 Buy - Trade 0.5 Sell = volume 0.5 (partial closing of position); - added volume — if a buy was performed for a long position:
Position 1.0 Buy + Trade 0.5 Buy = volume 1.5 (increase in position volume); - reversed — if a sell was performed for a long position with a volume greater than the position volume:
Position 1.0 Buy - Trade 1.5 Sell = Position 0.5 Sell (position reversal);
In case of a hedging account type, each deal can either modify an existing position or generate a new one. To change an existing position (closure or partial closure), we should specify the ID of the position you want to conduct a trading operation with in the trading order. Position ID is a unique number assigned to each new position remaining unchanged throughout the position lifetime:
- opening a new position — perform a buy or sell while another position still exists:
Position 1.0 Buy + Trade 1.0 Sell = 2 independent positions 1.0 Buy and 1.0 Sell (opening a position), or
Position 1.0 Buy + Trade 1.5 Buy = 2 independent positions 1.0 Buy and 1.5 Buy (opening a position), etc.; - closing an existing position — if a sell is performed with the ID of an already existing long position:
Position 1.0 Buy with ID 123 - Trade 1.0 Sell with ID 123 = closing 1.0 Buy with ID 123 (closing a position with ID 123); - partial closing of an existing position — if a sell is performed with the ID of an already existing long position with a volume less than the one of the existing position with the specified ID:
Position 1.0 Buy with ID 123 - Trade 0.5 Sell with ID 123 = volume 0.5 Buy with ID 123 (partial closing of a position with ID 123);
Find out more about orders, deals and positions in the article "Orders, Positions and Deals in MetaTrader 5".
Each order sent to the trade server remains in the list of active orders in the terminal until executed. When it is triggered, a deal appears, generating a new position or changing an existing one. At the moment the order is executed, it is placed in the list of historical orders, and a deal is created, which is also placed in the list of historical deals. If a deal creates a position, a new position is created and placed in the list of active positions.
There is no list of historical (closed) positions in the terminal. Also, there is no list of active deals in the terminal - they are immediately considered historical and are located in the corresponding list.
The explanation is simple: an order is an instruction to carry out trading operations on an account. It exists until it is fulfilled. It is located in the list of existing orders. Once an order is executed, it ceases to exist (ideally) and is placed in the list of executed trading orders. In doing so, it creates a deal. A deal is a fact of executing a trading order. This is a one-time event: an order is executed - we have a deal. That is all. A deal is added to the list of completed account events.
A deal creates a position that exists until it is closed. The position is always located in the list of active positions. Once it closes, it is removed from the list.
There is no list of closed positions in the terminal. But there is a list of completed deals. Therefore, in order to create a list of closed positions, it is necessary to make them up from deals belonging to a previously closed position. The list of deals is always available to us. The properties of each deal contain an ID of the appropriate position. Thus, we have access to everything necessary to recreate the life history of each of the positions that previously existed on the account - the position ID and the list of its deals.
Based on the above, we obtain the following algorithm for recreating previously closed positions from the existing historical list of deals:
- Get a list of all deals for the required symbol;
- Go through the list and get each next deal from the list;
- Сheck the position ID in the deal properties;
- If there is no position with such an ID in our own list of historical positions, create a new historical position object;
- Get the historical position object using the previously received ID;
- Add the current deal to the list of historical position object deals.
Upon completion of the loop through the list of terminal deals, we will have a complete list of all closed positions on the account by symbol, containing lists of deals belonging to each position.
Now, each historical position will feature a list of deals involved in changing this position, and we will be able to obtain complete information about all changes in a closed position during its existence.
To implement the logic described above, we need to create three classes:
- Deal class. The class will contain the list of all the properties of a historical deal, methods for accessing the properties - setting and getting, plus additional methods for displaying deal data;
- Position class. The class will contain the list of all deals, methods for accessing them and additional methods for displaying information about the position and its deals;
- Historical position management class. The class will contain the list of all historical positions, create and update it, as well as methods for accessing the properties of positions and their deals, plus additional methods for displaying information about positions and their deals.
Let's start.
Deal class
To get and save the properties of a deal, we need to select the history of orders and deals (HistorySelect()), go through the history of deals in a loop and get a deal ticket by the (HistoryDealGetTicket()) loop index from the list of historical deals. In this case, the deal will be selected to get its properties using the HistoryDealGetInteger(), HistoryDealGetDouble() and HistoryDealGetString() functions.
In the deal class, we will assume that the deal has already been selected and its properties can be immediately obtained. In addition to recording and retrieving the deal properties, the class will allow us to create a graphical label on a chart that displays the deal. The label will be drawn on canvas, so that we can draw the desired label for each type of deal instead of using labels from a predefined set of font symbols. When creating an object for each deal, we will receive a tick history of prices at the moment the deal appeared allowing us to calculate the spread at the moment the deal was carried out and display it in the description of the deal parameters.
Since each position can have many deals that participated in its change, all deals will ultimately be located in the class of the dynamic array of pointers to the instances of the CObject class and its CArrayObj descendants.
Accordingly, the deal class will be inherited from the Standard Library CObject base object class.
A deal has a set of integer, real and string properties. Each deal object will be in the list of objects in the historical position class. To implement the search for the required deal, it will be necessary to write an enumeration of parameters. Using the values of this enumeration, it will be possible to search for deals in the list and sort the list by these same values. We will need the search by only properties - deal ticket and time in milliseconds. The ticket will allow us to determine if such a deal is already present in the list or not. The time in milliseconds will enable us to arrange deals in strictly chronological order. But since classes imply extension, the list of deal properties will contain all of its properties.
To organize search and sorting in Standard Library CArrayObj lists, the Compare() virtual method returning the value of 0 is returned in the CObject class:
//--- method of comparing the objects virtual int Compare(const CObject *node,const int mode=0) const { return(0); }
In other words, this method always returns equality. In each of the objects of classes inherited from CObject, this method should be redefined, so that when comparing a property (specified in the mode parameter) of two compared objects, the method returns:
- -1 — if the value of the current object property is less than the one of the compared object;
- 1 — if the value of the current object property exceeds the one of the compared object;
- 0 — if the values of the specified property are equal for both compared objects.
Thus, based on the explanations above, for each of the classes created today, we will additionally need to create enumerations of the properties of class objects and methods for comparing them.
In the \MQL5\Include\ folder, create the PositionsViewer\ folder containing the new file Deal.mqh of the CDeal class:
CObject class of the Standard Library should be the base class:
As a result, we will get the following template for the deal object class file:
//+------------------------------------------------------------------+ //| 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" class CDeal : public CObject { private: public: CDeal(); ~CDeal(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CDeal::CDeal() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CDeal::~CDeal() { } //+------------------------------------------------------------------+
Include the files of the CObject library standard object classes and the CCanvas class to the created file of the deal object class and write the enumeration for sorting by deal object 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> #include <Canvas\Canvas.mqh> enum ENUM_DEAL_SORT_MODE { SORT_MODE_DEAL_TICKET = 0, // Mode of comparing/sorting by a deal ticket SORT_MODE_DEAL_ORDER, // Mode of comparing/sorting by the order a deal is based on SORT_MODE_DEAL_TIME, // Mode of comparing/sorting by a deal time SORT_MODE_DEAL_TIME_MSC, // Mode of comparing/sorting by a deal time in milliseconds SORT_MODE_DEAL_TYPE, // Mode of comparing/sorting by a deal type SORT_MODE_DEAL_ENTRY, // Mode of comparing/sorting by a deal direction SORT_MODE_DEAL_MAGIC, // Mode of comparing/sorting by a deal magic number SORT_MODE_DEAL_REASON, // Mode of comparing/sorting by a deal reason or source SORT_MODE_DEAL_POSITION_ID, // Mode of comparing/sorting by a position ID SORT_MODE_DEAL_VOLUME, // Mode of comparing/sorting by a deal volume SORT_MODE_DEAL_PRICE, // Mode of comparing/sorting by a deal price SORT_MODE_DEAL_COMMISSION, // Mode of comparing/sorting by commission SORT_MODE_DEAL_SWAP, // Mode of comparing/sorting by accumulated swap on close SORT_MODE_DEAL_PROFIT, // Mode of comparing/sorting by a deal financial result SORT_MODE_DEAL_FEE, // Mode of comparing/sorting by a deal fee SORT_MODE_DEAL_SL, // Mode of comparing/sorting by Stop Loss level SORT_MODE_DEAL_TP, // Mode of comparing/sorting by Take Profit level SORT_MODE_DEAL_SYMBOL, // Mode of comparing/sorting by a name of a traded symbol SORT_MODE_DEAL_COMMENT, // Mode of comparing/sorting by a deal comment SORT_MODE_DEAL_EXTERNAL_ID, // Mode of comparing/sorting by a deal ID in an external trading system }; //+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { }
Declare all the variables and methods necessary for the class to work in the private, protected and public sections:
//+------------------------------------------------------------------+ //| Deal class | //+------------------------------------------------------------------+ class CDeal : public CObject { private: MqlTick m_tick; // Deal tick structure //--- CCanvas object CCanvas m_canvas; // Canvas long m_chart_id; // Chart ID int m_width; // Canvas width int m_height; // Canvas height string m_name; // Graphical object name //--- Create a label object on the chart bool CreateLabelObj(void); //--- Draw (1) a mask, (2) Buy arrow on the canvas void DrawArrowMaskBuy(const int shift_x, const int shift_y); void DrawArrowBuy(const int shift_x, const int shift_y); //--- Draw (1) a mask, (2) Sell arrow on the canvas void DrawArrowMaskSell(const int shift_x, const int shift_y); void DrawArrowSell(const int shift_x, const int shift_y); //--- Draw the label appearance void DrawLabelView(void); //--- 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: //--- Integer properties long m_ticket; // Deal ticket. Unique number assigned to each deal long m_order; // Deal order number datetime m_time; // Deal execution time long m_time_msc; // Deal execution time in milliseconds since 01.01.1970 ENUM_DEAL_TYPE m_type; // Deal type ENUM_DEAL_ENTRY m_entry; // Deal entry - entry in, entry out, reverse long m_magic; // Magic number for a deal (see ORDER_MAGIC) ENUM_DEAL_REASON m_reason; // Deal execution reason or source long m_position_id; // The ID of the position opened, modified or closed by the deal //--- Real properties double m_volume; // Deal volume double m_price; // Deal price double m_commission; // Deal commission double m_swap; // Accumulated swap when closing double m_profit; // Deal financial result double m_fee; // Fee for making a deal charged immediately after performing a deal double m_sl; // Stop Loss level double m_tp; // Take Profit level //--- String properties string m_symbol; // Name of the symbol for which the deal is executed string m_comment; // Deal comment string m_external_id; // Deal ID in an external trading system (on the exchange) //--- 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 int m_spread; // Spread when performing a deal color m_color_arrow; // Deal label color //--- Draws an arrow corresponding to the deal type. It can be redefined in the inherited classes virtual void DrawArrow(void); public: //--- Set the properties //--- Integer properties void SetTicket(const long ticket) { this.m_ticket=ticket; } // Ticket void SetOrder(const long order) { this.m_order=order; } // Order void SetTime(const datetime time) { this.m_time=time; } // Time void SetTimeMsc(const long value) { this.m_time_msc=value; } // Time in milliseconds void SetTypeDeal(const ENUM_DEAL_TYPE type) { this.m_type=type; } // Type void SetEntry(const ENUM_DEAL_ENTRY entry) { this.m_entry=entry; } // Direction void SetMagic(const long magic) { this.m_magic=magic; } // Magic number void SetReason(const ENUM_DEAL_REASON reason) { this.m_reason=reason; } // Deal execution reason or source void SetPositionID(const long id) { this.m_position_id=id; } // Position ID //--- Real properties void SetVolume(const double volume) { this.m_volume=volume; } // Volume void SetPrice(const double price) { this.m_price=price; } // Price void SetCommission(const double value) { this.m_commission=value; } // Commission void SetSwap(const double value) { this.m_swap=value; } // Accumulated swap when closing void SetProfit(const double value) { this.m_profit=value; } // Financial result void SetFee(const double value) { this.m_fee=value; } // Deal fee void SetSL(const double value) { this.m_sl=value; } // Stop Loss level void SetTP(const double value) { this.m_tp=value; } // Take Profit level //--- String properties void SetSymbol(const string symbol) { this.m_symbol=symbol; } // Symbol name void SetComment(const string comment) { this.m_comment=comment; } // Comment void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } // Deal ID in an external trading system //--- Get the properties //--- Integer properties long Ticket(void) const { return(this.m_ticket); } // Ticket long Order(void) const { return(this.m_order); } // Order datetime Time(void) const { return(this.m_time); } // Time long TimeMsc(void) const { return(this.m_time_msc); } // Time in milliseconds ENUM_DEAL_TYPE TypeDeal(void) const { return(this.m_type); } // Type ENUM_DEAL_ENTRY Entry(void) const { return(this.m_entry); } // Direction long Magic(void) const { return(this.m_magic); } // Magic number ENUM_DEAL_REASON Reason(void) const { return(this.m_reason); } // Deal execution reason or source long PositionID(void) const { return(this.m_position_id); } // Position ID //--- Real properties double Volume(void) const { return(this.m_volume); } // Volume double Price(void) const { return(this.m_price); } // Price double Commission(void) const { return(this.m_commission); } // Commission double Swap(void) const { return(this.m_swap); } // Accumulated swap when closing double Profit(void) const { return(this.m_profit); } // Financial result double Fee(void) const { return(this.m_fee); } // Deal fee double SL(void) const { return(this.m_sl); } // Stop Loss level double TP(void) const { return(this.m_tp); } // Take Profit level 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(this.m_spread); } // Spread when performing a deal //--- String properties string Symbol(void) const { return(this.m_symbol); } // Symbol name string Comment(void) const { return(this.m_comment); } // Comment string ExternalID(void) const { return(this.m_external_id); } // Deal ID in an external trading system //--- Set the color of the deal label void SetColorArrow(const color clr); //--- (1) Hide, (2) display the deal label on a chart void HideArrow(const bool chart_redraw=false); void ShowArrow(const bool chart_redraw=false); //--- 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 (1) a short description and (2) a tooltip text of a deal string Description(void); virtual string Tooltip(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) { this.m_ticket=0; } CDeal(const ulong ticket); ~CDeal(); };
Let's consider the declared methods in details.
Parametric constructor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CDeal::CDeal(const ulong ticket) { //--- Store the properties //--- Integer properties this.m_ticket = (long)ticket; // Deal ticket this.m_order = ::HistoryDealGetInteger(ticket, DEAL_ORDER); // Order this.m_time = (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME); // Deal execution time this.m_time_msc = ::HistoryDealGetInteger(ticket, DEAL_TIME_MSC); // Deal execution time in milliseconds this.m_type = (ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE); // Type this.m_entry = (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY); // Direction this.m_magic = ::HistoryDealGetInteger(ticket, DEAL_MAGIC); // Magic number this.m_reason = (ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON); // Deal execution reason or source this.m_position_id= ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID); // Position ID //--- Real properties this.m_volume = ::HistoryDealGetDouble(ticket, DEAL_VOLUME); // Volume this.m_price = ::HistoryDealGetDouble(ticket, DEAL_PRICE); // Price this.m_commission = ::HistoryDealGetDouble(ticket, DEAL_COMMISSION); // Commission this.m_swap = ::HistoryDealGetDouble(ticket, DEAL_SWAP); // Accumulated swap when closing this.m_profit = ::HistoryDealGetDouble(ticket, DEAL_PROFIT); // Financial result this.m_fee = ::HistoryDealGetDouble(ticket, DEAL_FEE); // Deal fee this.m_sl = ::HistoryDealGetDouble(ticket, DEAL_SL); // Stop Loss level this.m_tp = ::HistoryDealGetDouble(ticket, DEAL_TP); // Take Profit level //--- String properties this.m_symbol = ::HistoryDealGetString(ticket, DEAL_SYMBOL); // Symbol name this.m_comment = ::HistoryDealGetString(ticket, DEAL_COMMENT); // Comment this.m_external_id= ::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID); // Deal ID in an external trading system //--- Graphics display parameters this.m_chart_id = ::ChartID(); this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_width = 19; this.m_height = 19; this.m_name = "Deal#"+(string)this.m_ticket; this.m_color_arrow= (this.TypeDeal()==DEAL_TYPE_BUY ? C'3,95,172' : this.TypeDeal()==DEAL_TYPE_SELL ? C'225,68,29' : C'180,180,180'); //--- Parameters for calculating spread this.m_spread = 0; this.m_bid = 0; this.m_ask = 0; //--- Create a graphic label this.CreateLabelObj(); //--- 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; this.m_spread=int((this.m_ask-this.m_bid)/this.m_point); } //--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on else this.m_spread=this.GetSpreadM1(); }
Keep in mind that the current deal has already been selected. Therefore, here we immediately fill in all the object properties from the deal properties. Next, set the canvas parameters for displaying the deal label on the chart, create a graphical object of this label and get the tick by the time of the deal to calculate the spread present at the time of the deal. If failed to get a tick, take the average spread of the minute bar the deal took place on.
As a result, when creating a deal object, we immediately receive an object with the properties of a historical deal set in it according to its ticket, as well as an already created but hidden label for visualizing the deal on a chart, plus the spread value at the time of the deal.
In the class destructor, check the deinitialization reason. If this is not a change in the chart timeframe, destroy the graphical resource of the graphical label object:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CDeal::~CDeal() { if(::UninitializeReason()!=REASON_CHARTCHANGE) this.m_canvas.Destroy(); }
The virtual method for comparing two objects with each other 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 SORT_MODE_DEAL_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_DEAL_ORDER : return(this.Order() > obj.Order() ? 1 : this.Order() < obj.Order() ? -1 : 0); case SORT_MODE_DEAL_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_DEAL_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_DEAL_TYPE : return(this.TypeDeal() > obj.TypeDeal() ? 1 : this.TypeDeal() < obj.TypeDeal() ? -1 : 0); case SORT_MODE_DEAL_ENTRY : return(this.Entry() > obj.Entry() ? 1 : this.Entry() < obj.Entry() ? -1 : 0); case SORT_MODE_DEAL_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_DEAL_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_DEAL_POSITION_ID : return(this.PositionID() > obj.PositionID() ? 1 : this.PositionID() < obj.PositionID() ? -1 : 0); case SORT_MODE_DEAL_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_DEAL_PRICE : return(this.Price() > obj.Price() ? 1 : this.Price() < obj.Price() ? -1 : 0); case SORT_MODE_DEAL_COMMISSION : return(this.Commission() > obj.Commission() ? 1 : this.Commission() < obj.Commission() ? -1 : 0); case SORT_MODE_DEAL_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_DEAL_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_DEAL_FEE : return(this.Fee() > obj.Fee() ? 1 : this.Fee() < obj.Fee() ? -1 : 0); case SORT_MODE_DEAL_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_DEAL_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_DEAL_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_DEAL_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_DEAL_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); default : return(-1); } }
The method receives the pointer to the compared object and the value of the compared property from the ENUM_DEAL_SORT_MODE enumeration. If the value of the specified property of the current object is greater than the value of the compared object property, 1 is returned. If the value of the current object property is less than the value of the compared objects, -1 is returned. In any other case, we have zero.
The method that returns a deal type description:
//+------------------------------------------------------------------+ //| Return the deal type description | //+------------------------------------------------------------------+ string CDeal::TypeDescription(void) const { switch(this.m_type) { 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.m_type; } }
Descriptions of all deal types are returned here. Most of them will not be used in the program. But the method returns all types, taking into account the possibilities of extension and inheritance of classes.
The method returning a description of the position change method:
//+------------------------------------------------------------------+ //| Return position change method | //+------------------------------------------------------------------+ string CDeal::EntryDescription(void) const { switch(this.m_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.m_entry; } }
The method returning a deal reason description:
//+------------------------------------------------------------------+ //| Return a deal reason description | //+------------------------------------------------------------------+ string CDeal::ReasonDescription(void) const { switch(this.m_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.m_reason; } }
We will use only three values from the method: closing by StopLoss, TakeProfit and StopOut.
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()))); }
Use the StringFormat() function to create and return the following string:
Deal: Entry In 0.10 Buy #1728374638 at 2023.06.12 16:51:36.838
The virtual method returning a text of a deal pop-up message:
//+------------------------------------------------------------------+ //| Returns a text of a deal pop-up message | //+------------------------------------------------------------------+ string CDeal::Tooltip(void) { return(::StringFormat("Position ID #%I64d %s:\nDeal #%I64d %.2f %s %s\n%s [%.*f]\nProfit: %.2f, SL: %.*f, TP: %.*f", this.PositionID(), this.Symbol(), this.Ticket(), this.Volume(), this.TypeDescription(), this.EntryDescription(), this.TimeMscToString(this.TimeMsc()), this.m_digits, this.Price(), this.Profit(), this.m_digits, this.SL(), this.m_digits, this.TP())); }
Similar to the method discussed above, here a text string of the following type is formed and returned:
Position ID #1752955040 EURUSD: Deal #1728430603 0.10 Sell Entry Out 2023.06.12 17:04:20.362 [1.07590] Profit: 15.00, SL: 1.07290, TP: 1.07590
This string will be used later to display a deal description in a tooltip when hovering the mouse pointer over the deal label on the chart. The method can be redefined in inherited classes to output other deal information.
The method that prints deal properties in the journal:
//+------------------------------------------------------------------+ //| Print deal properties in the journal | //+------------------------------------------------------------------+ void CDeal::Print(void) { ::PrintFormat(" %s", this.Description()); }
The method does not print all the deal properties, but only displays the minimum information about the deal returned by the Description() method discussed above. Two spaces are added before the deal description string to arrange the information when displaying a position description, in which a position description header is followed by data on position deals:
Position EURUSD 0.10 Buy #1752955040, Magic 0 -Opened 2023.06.12 16:51:36.838 [1.07440] -Closed 2023.06.12 17:04:20.362 [1.07590] Deal: Entry In 0.10 Buy #1728374638 at 2023.06.12 16:51:36.838 Deal: Entry Out 0.10 Sell #1728430603 at 2023.06.12 17:04:20.362
Find out more about the PrintFormat() and StringFormat() functions in the articles "Studying PrintFormat() and applying ready-made examples" and "StringFormat(). Review and ready-made examples".
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')); }
Here we form a string containing the time value in milliseconds converted to normal time by dividing by 1000. The number of milliseconds obtained as the remainder of dividing the time in milliseconds by 1000 is added to the resulting string after a dot. The milliseconds string is formatted as a three-digit number with leading zeros added if it is shorter than three digits. As a result, we get the following representation of time:
2023.06.12 17:04:20.362
In order to understand what the spread was at the time of the deal, it is necessary to obtain the required tick for the deal time in milliseconds. A seemingly trivial task of copying one tick at a known time using the standard CopyTicks() function resulted in a small quest to find a solution, since it was not possible to copy the tick at the exact time specified. As a result, after some searching for a solution, I managed to find the necessary algorithm: we need to make a certain number of tick requests in an ever-expanding range of time "From" to the deal time. More on this here (in Russian).
The method to get the deal tick:
//+------------------------------------------------------------------+ //| Get the deal tick | //+------------------------------------------------------------------+ 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.m_symbol, ticks, COPY_TICKS_INFO, this.m_time_msc-(offset <<=1), this.m_time_msc); //--- 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); }
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.m_symbol, PERIOD_M1, this.Time()); if(bar==WRONG_VALUE) return 0; return(::CopySpread(this.m_symbol, PERIOD_M1, bar, 1, array)==1 ? array[0] : 0); }
Here we get the opening time of the minute bar by the deal time, which is used to get the average spread of the bar using the CopySpread() function. If there is an error getting a bar or spread, the method returns zero.
The method that creates a label object on a chart:
//+------------------------------------------------------------------+ //| Create a label object on the chart | //+------------------------------------------------------------------+ bool CDeal::CreateLabelObj(void) { //--- Create a graphical resource with a Bitmap object attached to it ::ResetLastError(); if(!this.m_canvas.CreateBitmap(this.m_name, this.m_time, this.m_price, this.m_width, this.m_height, COLOR_FORMAT_ARGB_NORMALIZE)) { ::PrintFormat("%s: When creating a graphic object, error %d occurred in the CreateBitmap method of the CCanvas class",__FUNCTION__, ::GetLastError()); return false; } //--- If the graphical resource is successfully created, set the Bitmap object, anchor point, price, time and tooltip text ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_ANCHOR, ANCHOR_CENTER); ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIME, this.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_name, OBJPROP_PRICE, this.Price()); ::ObjectSetString(this.m_chart_id, this.m_name, OBJPROP_TOOLTIP, this.Tooltip()); //--- Hide the created object from the chart and draw its appearance on it this.HideArrow(); this.DrawLabelView(); return true; }
Create a graphical resource, set its anchor point to the center, the deal price and time and the tooltip text. Next, a flag is set for the created object indicating that it is hidden on all chart periods, and its appearance is drawn on it. Now, to display it, we just need to set the visibility flag for the object on all timeframes.
The method that draws the appearance of the label object:
//+------------------------------------------------------------------+ //| Draw the appearance of the label object | //+------------------------------------------------------------------+ void CDeal::DrawLabelView(void) { this.m_canvas.Erase(0x00FFFFFF); this.DrawArrow(); this.m_canvas.Update(true); }
First, the Bitmap graphical object is filled with a completely transparent color, then an arrow corresponding to the deal type is drawn on it, and then the canvas is updated while the chart is redrawn.
The virtual method drawing the arrow corresponding to the deal type:
//+------------------------------------------------------------------+ //| Draw an arrow corresponding to the deal type | //+------------------------------------------------------------------+ void CDeal::DrawArrow(void) { switch(this.TypeDeal()) { case DEAL_TYPE_BUY : this.DrawArrowBuy(5, 10); break; case DEAL_TYPE_SELL : this.DrawArrowSell(5, 0); break; default : break; } }
Depending on the deal type (Buy or Sell), the method for drawing the corresponding arrow is called. The method is virtual, so it can be redefined in the inherited classes. For example, in a child class, we can draw a different label for a different type of deal, or you can additionally take into account the method of changing the position, etc.
The method drawing the Buy arrow mask on the canvas:
//+------------------------------------------------------------------+ //| Draw Buy arrow mask on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowMaskBuy(const int shift_x, const int shift_y) { int x[]={4+shift_x, 8+shift_x, 8+shift_x, 6+shift_x, 6+shift_x, 2+shift_x, 2+shift_x, 0+shift_x, 0+shift_x, 4+shift_x}; int y[]={0+shift_y, 4+shift_y, 5+shift_y, 5+shift_y, 7+shift_y, 7+shift_y, 5+shift_y, 5+shift_y, 4+shift_y, 0+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220)); }
The arrows drawn as deal labels have a white outline (mask) around the perimeter to stand out against the dark background of the candle:
Since the coordinates of figures drawn on the canvas are always specified relative to the local coordinates of the canvas, it is necessary to enter offsets that will be added to the coordinates of the broken line points along the X and Y axes, so that we can accurately center the figure being drawn inside the canvas. Next, taking into account the offsets passed to the method, the values of the X and Y coordinates are set in the arrays and the Polygon() method is called to draw a white outline using coordinate points. Next, the arrow will be drawn inside the outline using the arrow drawing methods.
The method drawing the Buy arrow on the canvas:
//+------------------------------------------------------------------+ //| Draw Buy arrow on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowBuy(const int shift_x, const int shift_y) { this.DrawArrowMaskBuy(shift_x, shift_y); int x[]={4+shift_x, 7+shift_x, 5+shift_x, 5+shift_x, 3+shift_x, 3+shift_x, 1+shift_x, 4+shift_x}; int y[]={1+shift_y, 4+shift_y, 4+shift_y, 6+shift_y, 6+shift_y, 4+shift_y, 4+shift_y, 1+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow)); }
Here we first draw the Buy arrow mask, then we draw the Buy arrow using the specified coordinates and fill its internal space with color.
Similar methods are applied for drawing the Sell arrow:
//+------------------------------------------------------------------+ //| Draw Sell arrow mask on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowMaskSell(const int shift_x, const int shift_y) { int x[]={4+shift_x, 0+shift_x, 0+shift_x, 2+shift_x, 2+shift_x, 6+shift_x, 6+shift_x, 8+shift_x, 8+shift_x, 4+shift_x}; int y[]={8+shift_y, 4+shift_y, 3+shift_y, 3+shift_y, 1+shift_y, 1+shift_y, 3+shift_y, 3+shift_y, 4+shift_y, 8+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220)); } //+------------------------------------------------------------------+ //| Draw Sell arrow on the canvas | //+------------------------------------------------------------------+ void CDeal::DrawArrowSell(const int shift_x, const int shift_y) { this.DrawArrowMaskSell(shift_x, shift_y); int x[]={4+shift_x, 1+shift_x, 3+shift_x, 3+shift_x, 5+shift_x, 5+shift_x, 7+shift_x, 4+shift_x}; int y[]={7+shift_y, 4+shift_y, 4+shift_y, 2+shift_y, 2+shift_y, 4+shift_y, 4+shift_y, 7+shift_y}; this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow)); this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow)); }
Methods for hiding and displaying the deal label on the chart:
//+------------------------------------------------------------------+ //| Hide the deal label on the chart | //+------------------------------------------------------------------+ void CDeal::HideArrow(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| Display the deal label on the chart | //+------------------------------------------------------------------+ void CDeal::ShowArrow(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
To hide an object on the chart, the OBJPROP_TIMEFRAMES visibility properties of the graphical object should be set to OBJ_NO_PERIODS.
Accordingly, to display the object, we need to set the OBJPROP_TIMEFRAMES property to OBJ_ALL_PERIODS.
The method that sets the deal label color:
//+------------------------------------------------------------------+ //| Set the deal label color | //+------------------------------------------------------------------+ void CDeal::SetColorArrow(const color clr) { this.m_color_arrow=clr; this.DrawLabelView(); }
The m_color_arrow variable storing the drawn label color is set to the value passed to the method, and the entire graphical object is completely redrawn. Thus, it is possible to change the color of the deal label on the chart from the control program "on the fly".
We have created a deal class that provides access to the properties of a historical deal by its ticket, and allows us to get the necessary data on this deal and display or hide its label on the chart.
The class objects will be stored in the list of position object deals, which in turn will display the properties of the historical position, provide access to its deals and parameters, and display opening and closing deals connected by a line.
Position class
Create a new file Position.mqh of the CPosition class in the same folder where the deal class has been created.
Similar to the deal class, implement the enumeration of object properties to search and sort by position properties and connect deal class files and CArrayObj for the position 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" #include "Deal.mqh" #include <Arrays\ArrayObj.mqh> enum ENUM_POSITION_SORT_MODE { SORT_MODE_POSITION_TICKET = 0, // Mode of comparing/sorting by a position ticket SORT_MODE_POSITION_TIME, // Mode of comparing/sorting by position open time SORT_MODE_POSITION_TIME_MSC, // Mode of comparing/sorting by position open time im milliseconds SORT_MODE_POSITION_TIME_UPDATE, // Mode of comparing/sorting by position update time SORT_MODE_POSITION_TIME_UPDATE_MSC, // Mode of comparing/sorting by position update time im milliseconds SORT_MODE_POSITION_TYPE, // Mode of comparing/sorting by position type SORT_MODE_POSITION_MAGIC, // Mode of comparing/sorting by a position magic number SORT_MODE_POSITION_IDENTIFIER, // Mode of comparing/sorting by a position ID SORT_MODE_POSITION_REASON, // Mode of comparing/sorting by position open reason SORT_MODE_POSITION_VOLUME, // Mode of comparing/sorting by a position volume SORT_MODE_POSITION_PRICE_OPEN, // Mode of comparing/sorting by a position price SORT_MODE_POSITION_SL, // Mode of comparing/sorting by Stop Loss level for an open position SORT_MODE_POSITION_TP, // Mode of comparing/sorting by Take Profit level for an open position SORT_MODE_POSITION_PRICE_CURRENT, // Mode of comparing/sorting by the current symbol price SORT_MODE_POSITION_SWAP, // Mode of comparing/sorting by accumulated swap SORT_MODE_POSITION_PROFIT, // Mode of comparing/sorting by the current profit SORT_MODE_POSITION_SYMBOL, // Mode of comparing/sorting by a symbol a position is opened on SORT_MODE_POSITION_COMMENT, // Mode of comparing/sorting by a position comment SORT_MODE_POSITION_EXTERNAL_ID, // Mode of comparing/sorting by a position ID in an external system SORT_MODE_POSITION_TIME_CLOSE, // Mode of comparing/sorting by a position open time SORT_MODE_POSITION_TIME_CLOSE_MSC, // Mode of comparing/sorting by a position open time in milliseconds SORT_MODE_POSITION_PRICE_CLOSE, // Mode of comparing/sorting by a position price }; //+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { }
In the protected and public sections of the class, enter variables and methods for handling the class:
//+------------------------------------------------------------------+ //| Position class | //+------------------------------------------------------------------+ class CPosition : public CObject { private: 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; //--- Integer properties long m_ticket; // Position ticket datetime m_time; // Position opening time long m_time_msc; // Position opening time in milliseconds since 01.01.1970 datetime m_time_update; // Position update time long m_time_update_msc; // Position update time in milliseconds since 01.01.1970 ENUM_POSITION_TYPE m_type; // Position type long m_magic; // Magic number for a position (see ORDER_MAGIC) long m_identifier; // Position ID ENUM_POSITION_REASON m_reason; // Position opening reason //--- Real properties double m_volume; // Position volume double m_price_open; // Position price double m_sl; // Stop Loss level for an open position double m_tp; // Take Profit level for an open position double m_price_current; // Current price by symbol double m_swap; // Accumulated swap double m_profit; // Current profit //--- String properties string m_symbol; // A symbol the position is open for string m_comment; // Position comment string m_external_id; // Position ID in an external system (on the exchange) //--- Additional properties long m_chart_id; // Chart ID int m_profit_pt; // Profit in points int m_digits; // Symbol digits double m_point; // One symbol point value double m_contract_size; // Symbol trade contract size string m_currency_profit; // Symbol profit currency string m_account_currency; // Deposit currency string m_line_name; // Line graphical object name color m_line_color; // Connecting line color //--- Create a line connecting open-close deals virtual bool CreateLine(void); //--- Return the pointer to (1) open and (2) close deal CDeal *GetDealIn(void) const; CDeal *GetDealOut(void) const; //--- (1) Hide and (2) display deal labels on the chart void HideDeals(const bool chart_redraw=false); void ShowDeals(const bool chart_redraw=false); //--- (1) Hide and (2) display the connecting line between the deal labels void HideLine(const bool chart_redraw=false); void ShowLine(const bool chart_redraw=false); public: //--- Set the properties //--- Integer properties void SetTicket(const long ticket) { this.m_ticket=ticket; } // Position ticket void SetTime(const datetime time) { this.m_time=time; } // Position open time void SetTimeMsc(const long value) { this.m_time_msc=value; } // Position open time in milliseconds 01.01.1970 void SetTimeUpdate(const datetime time) { this.m_time_update=time; } // Position update time void SetTimeUpdateMsc(const long value) { this.m_time_update_msc=value; } // Position update time in milliseconds 01.01.1970 void SetTypePosition(const ENUM_POSITION_TYPE type) { this.m_type=type; } // Position type void SetMagic(const long magic) { this.m_magic=magic; } // Magic number for a position (see ORDER_MAGIC) void SetID(const long id) { this.m_identifier=id; } // Position ID void SetReason(const ENUM_POSITION_REASON reason) { this.m_reason=reason; } // Position opening reason //--- Real properties void SetVolume(const double volume) { this.m_volume=volume; } // Position volume void SetPriceOpen(const double price) { this.m_price_open=price; } // Position price void SetSL(const double value) { this.m_sl=value; } // Stop Loss level for an open position void SetTP(const double value) { this.m_tp=value; } // Take Profit level for an open position void SetPriceCurrent(const double price) { this.m_price_current=price; } // Current price by symbol void SetSwap(const double value) { this.m_swap=value; } // Accumulated swap void SetProfit(const double value) { this.m_profit=value; } // Current profit //--- String properties void SetSymbol(const string symbol) { this.m_symbol=symbol; } // Symbol a position is opened for void SetComment(const string comment) { this.m_comment=comment; } // Position comment void SetExternalID(const string ext_id) { this.m_external_id=ext_id; } // Position ID in an external system (on the exchange) //--- Get the properties //--- Integer properties long Ticket(void) const { return(this.m_ticket); } // Position ticket datetime Time(void) const { return(this.m_time); } // Position open time long TimeMsc(void) const { return(this.m_time_msc); } // Position open time in milliseconds since 01.01.1970 datetime TimeUpdate(void) const { return(this.m_time_update); } // Position update time long TimeUpdateMsc(void) const { return(this.m_time_update_msc);} // Position update time in milliseconds since 01.01.1970 ENUM_POSITION_TYPE TypePosition(void) const { return(this.m_type); } // Position type long Magic(void) const { return(this.m_magic); } // Magic number for a position (see ORDER_MAGIC) long ID(void) const { return(this.m_identifier); } // Position ID ENUM_POSITION_REASON Reason(void) const { return(this.m_reason); } // Position opening reason //--- Real properties double Volume(void) const { return(this.m_volume); } // Position volume double PriceOpen(void) const { return(this.m_price_open); } // Position price double SL(void) const { return(this.m_sl); } // Stop Loss level for an open position double TP(void) const { return(this.m_tp); } // Take Profit for an open position double PriceCurrent(void) const { return(this.m_price_current); } // Current price by symbol double Swap(void) const { return(this.m_swap); } // Accumulated swap double Profit(void) const { return(this.m_profit); } // Current profit //--- String properties string Symbol(void) const { return(this.m_symbol); } // A symbol position is opened on string Comment(void) const { return(this.m_comment); } // Position comment string ExternalID(void) const { return(this.m_external_id); } // Position ID in an external system (on the exchange) //--- Additional properties ulong DealIn(void) const; // Open deal ticket ulong DealOut(void) const; // Close deal ticket datetime TimeClose(void) const; // Close time long TimeCloseMsc(void) const; // Close time in milliseconds int ProfitInPoints(void) const; // Profit in points double PriceClose(void) const; // Close price //--- Add a deal to the list of deals, return the pointer CDeal *DealAdd(const long ticket); //--- Set the color of the (1) connecting line, (2) Buy and Sell deals void SetLineColor(const color clr=C'225,68,29'); void SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29'); //--- 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 a brief position description string Description(void); //--- Returns a text of a position popup message virtual string Tooltip(void); //--- Print the properties of the position and its deals in the journal void Print(void); //--- (1) Hide and (2) display a graphical representation of a position on a chart void Hide(const bool chart_redraw=false); void Show(const bool chart_redraw=false); //--- 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) { this.m_symbol=::Symbol(); } ~CPosition(); };
In order to search in the sorted list in the class of the dynamic array of pointers to the instances of the CObject class and its descendants CArrayObj, the Search() method receives the pointer to the instance of the object, in which we should perform a comparison by the property used to sort the list. In order to avoid constantly creating and destroying a new object with a given property, which is quite expensive in terms of performance, an instance of a deal object is declared in the protected section of the class. To perform a search, we will simply set the required value of the required property in this object and pass it to the search method as the desired instance.
Let's take a closer look at the declared methods.
Parametric constructor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPosition::CPosition(const long position_id, const string symbol) { this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); this.m_identifier = position_id; this.m_account_currency = ::AccountInfoString(ACCOUNT_CURRENCY); this.m_symbol = (symbol==NULL ? ::Symbol() : symbol); this.m_digits = (int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS); this.m_point = ::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT); this.m_contract_size = ::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE); this.m_currency_profit = ::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT); this.m_chart_id = ::ChartID(); this.m_line_name = "line#"+(string)this.m_identifier; this.m_line_color = C'225,68,29'; }
The constructor receives the position ID and the symbol the position was opened for. The list of position deals receives the flag of sorting by a deal time in milliseconds. The ID and symbol are saved in the class variables, while some parameters of the account and symbol, as well as chart ID and properties of the line, connecting the position opening and closing deal labels, are set.
In the class destructor, remove all graphical objects whose prefix is the line name and clear the list of deals:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPosition::~CPosition() { ::ObjectDelete(this.m_chart_id, this.m_line_name); this.m_list_deals.Clear(); }
The deletion of the graphical line object is done by prefix for the following reasons: if the inherited class is to display several lines connecting all the position deals, and not just the opening and closing ones, then the name of each line can be set as "line_name" + "line_number". In this case, all such lines will still be deleted in the destructor, since they all have one common prefix - "line name".
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 SORT_MODE_POSITION_TICKET : return(this.Ticket() > obj.Ticket() ? 1 : this.Ticket() < obj.Ticket() ? -1 : 0); case SORT_MODE_POSITION_TIME : return(this.Time() > obj.Time() ? 1 : this.Time() < obj.Time() ? -1 : 0); case SORT_MODE_POSITION_TIME_MSC : return(this.TimeMsc() > obj.TimeMsc() ? 1 : this.TimeMsc() < obj.TimeMsc() ? -1 : 0); case SORT_MODE_POSITION_TIME_UPDATE : return(this.TimeUpdate() > obj.TimeUpdate() ? 1 : this.TimeUpdate() < obj.TimeUpdate() ? -1 : 0); case SORT_MODE_POSITION_TIME_UPDATE_MSC: return(this.TimeUpdateMsc() > obj.TimeUpdateMsc() ? 1 : this.TimeUpdateMsc() < obj.TimeUpdateMsc() ? -1 : 0); case SORT_MODE_POSITION_TYPE : return(this.TypePosition() > obj.TypePosition() ? 1 : this.TypePosition() < obj.TypePosition() ? -1 : 0); case SORT_MODE_POSITION_MAGIC : return(this.Magic() > obj.Magic() ? 1 : this.Magic() < obj.Magic() ? -1 : 0); case SORT_MODE_POSITION_IDENTIFIER : return(this.ID() > obj.ID() ? 1 : this.ID() < obj.ID() ? -1 : 0); case SORT_MODE_POSITION_REASON : return(this.Reason() > obj.Reason() ? 1 : this.Reason() < obj.Reason() ? -1 : 0); case SORT_MODE_POSITION_VOLUME : return(this.Volume() > obj.Volume() ? 1 : this.Volume() < obj.Volume() ? -1 : 0); case SORT_MODE_POSITION_PRICE_OPEN : return(this.PriceOpen() > obj.PriceOpen() ? 1 : this.PriceOpen() < obj.PriceOpen() ? -1 : 0); case SORT_MODE_POSITION_SL : return(this.SL() > obj.SL() ? 1 : this.SL() < obj.SL() ? -1 : 0); case SORT_MODE_POSITION_TP : return(this.TP() > obj.TP() ? 1 : this.TP() < obj.TP() ? -1 : 0); case SORT_MODE_POSITION_PRICE_CURRENT : return(this.PriceCurrent() > obj.PriceCurrent() ? 1 : this.PriceCurrent() < obj.PriceCurrent() ? -1 : 0); case SORT_MODE_POSITION_SWAP : return(this.Swap() > obj.Swap() ? 1 : this.Swap() < obj.Swap() ? -1 : 0); case SORT_MODE_POSITION_PROFIT : return(this.Profit() > obj.Profit() ? 1 : this.Profit() < obj.Profit() ? -1 : 0); case SORT_MODE_POSITION_SYMBOL : return(this.Symbol() > obj.Symbol() ? 1 : this.Symbol() < obj.Symbol() ? -1 : 0); case SORT_MODE_POSITION_COMMENT : return(this.Comment() > obj.Comment() ? 1 : this.Comment() < obj.Comment() ? -1 : 0); case SORT_MODE_POSITION_EXTERNAL_ID : return(this.ExternalID() > obj.ExternalID() ? 1 : this.ExternalID() < obj.ExternalID() ? -1 : 0); case SORT_MODE_POSITION_TIME_CLOSE : return(this.TimeClose() > obj.TimeClose() ? 1 : this.TimeClose() < obj.TimeClose() ? -1 : 0); case SORT_MODE_POSITION_TIME_CLOSE_MSC : return(this.TimeCloseMsc() > obj.TimeCloseMsc() ? 1 : this.TimeCloseMsc() < obj.TimeCloseMsc() ? -1 : 0); case SORT_MODE_POSITION_PRICE_CLOSE : return(this.PriceClose() > obj.PriceClose() ? 1 : this.PriceClose() < obj.PriceClose() ? -1 : 0); default : return -1; } }
A similar method was considered when developing the deal object class. Here everything is similar: if the current object property value is greater than the one being compared, 1 is returned; if it is less, we get -1; if the properties are equal, we have zero.
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')); }
This is a copy of the same-name method of the deal object class considered in the deal class.
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; }
We need to find a deal with the Entry In position update method in the list of deal objects. This deal should be first in a time-sorted list. Therefore, we start the cycle from the beginning of the list - from index 0.
We get a deal from the list by index and, if this is a market entry deal, return the pointer to the deal found in the list. At the end of the cycle, return NULL — deal not found.
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; }
Here, compared to the previous method, everything is the other way around - the deal to exit the position is at the very end of the list, so we start the cycle from the end.
As soon as a deal to exit a position is found, return the pointer to it. Otherwise, return NULL.
The methods for obtaining additional properties of a historical position:
//+------------------------------------------------------------------+ //| Return the open deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealIn(void) const { CDeal *deal=this.GetDealIn(); return(deal!=NULL ? deal.Ticket() : 0); } //+------------------------------------------------------------------+ //| Return the close deal ticket | //+------------------------------------------------------------------+ ulong CPosition::DealOut(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Ticket() : 0); } //+------------------------------------------------------------------+ //| Return the close time | //+------------------------------------------------------------------+ datetime CPosition::TimeClose(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Time() : 0); } //+------------------------------------------------------------------+ //| Return the close time in milliseconds | //+------------------------------------------------------------------+ long CPosition::TimeCloseMsc(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.TimeMsc() : 0); } //+------------------------------------------------------------------+ //| Return the close price | //+------------------------------------------------------------------+ double CPosition::PriceClose(void) const { CDeal *deal=this.GetDealOut(); return(deal!=NULL ? deal.Price() : 0); }
In all methods, we first get the pointer to a deal, from which we need to take the corresponding property, and if the pointer is valid, we return the property value, otherwise - zero.
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(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point); }
Here we first obtain the open and close prices from the corresponding deals, and then return the result of the profit calculation in points, depending on the position direction.
The method that adds a deal to the list of 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(SORT_MODE_DEAL_TICKET); //--- Set the result of checking if a deal with such a ticket is present in the list bool added=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE); //--- Set the flag of sorting by time in milliseconds for the list this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC); //--- If a deal with such a ticket is already in the list, return NULL if(added) 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 and //--- create a connecting line between the position open-close deal labels if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { this.SetProfit(deal.Profit()); this.CreateLine(); } //--- Return the pointer to the created deal object return deal; }
The method receives the ticket of a selected deal. First, make sure there is no deal with such a ticket in the list. To do this, the temporary deal object is assigned the ticket passed to the method. The list of deals is sorted by the value of the deal tickets, and a search is performed for a deal in the list with such a ticket. Immediately after searching for a deal, the flag of sorting by time in milliseconds is returned to the list. If such a deal is already in the list, there is no need to add it - the method returns NULL. At the same time, the sorting of the deal list is already set by default - by time in milliseconds. If such a deal is not in the list, a new deal object is created and added to the list in the order of sorting by time in milliseconds. If this is a position close deal, then its profit is set in the position properties and a connecting line is created between the position open and close deals. As a result, the pointer to the newly created deal object is returned.
The method that returns a position type description:
//+------------------------------------------------------------------+ //| Return a position type description | //+------------------------------------------------------------------+ string CPosition::TypeDescription(void) const { return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.m_type); }
Return the description depending on the type of a position stored in the m_type variable. If the variable value does not match the position type enumeration constants, return "Unknown::"+variable_value.
The method returning position close time and price description:
//+------------------------------------------------------------------+ //| 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())); }
If there is no close deal in the position object yet, the method returns a message that the position is not yet closed. Otherwise, a string of the following type is created and returned:
Closed 2023.06.12 17:04:20.362 [1.07590]
The method returning the description of the position open time and price:
//+------------------------------------------------------------------+ //| 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())); }
A string of the following type is created and returned here:
Opened 2023.06.12 16:51:36.838 [1.07440]
The method returning a brief position description:
//+------------------------------------------------------------------+ //| Return a brief position description | //+------------------------------------------------------------------+ string CPosition::Description(void) { return(::StringFormat("Position %s %.2f %s #%I64d, Magic %I64d", this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic())); }
Create and return a string of the following type:
Position EURUSD 0.10 Buy #1752955040, Magic 123
The virtual method returning a text of a position pop-up message:
//+------------------------------------------------------------------+ //| Return a text of a position pop-up message | //+------------------------------------------------------------------+ string CPosition::Tooltip(void) { //--- Get the pointers to the open and close deals CDeal *deal_in =this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); //--- If no deals are received, return an empty string if(deal_in==NULL || deal_out==NULL) return NULL; //--- Get commission, swap and deal fee that are common for two deals double commission=deal_in.Commission()+deal_out.Commission(); double swap=deal_in.Swap()+deal_out.Swap(); double fee=deal_in.Fee()+deal_out.Fee(); //--- Get the final profit of the position and the spread values when opening and closing double profit=deal_out.Profit(); int spread_in=deal_in.Spread(); int spread_out=deal_out.Spread(); //--- If the reason for closing the position is StopLoss, TakeProfit or StopOut, set the reason description in the variable string reason=(deal_out.Reason()==DEAL_REASON_SL || deal_out.Reason()==DEAL_REASON_TP || deal_out.Reason()==DEAL_REASON_SO ? deal_out.ReasonDescription() : ""); //--- Create and return the tooltip string return(::StringFormat("%s\nCommission %.2f, Swap %.2f, Fee %.2f\nSpread In/Out %d/%d, Profit %+.2f %s (%d points)\nResult: %s %+.2f %s", this.Description(), commission, swap, fee, spread_in, spread_out, profit,this.m_currency_profit, this.ProfitInPoints(), reason, profit+commission+fee+swap, this.m_currency_profit)); }
A string of the following type is created and returned in the method:
Position EURUSD 0.10 Buy #1752955040, Magic 0 Commission 0.00, Swap 0.00, Fee 0.00 Spread In/Out 0/0, Profit +15.00 USD (150 points) Result: TP +15.00 USD
The text is set as the tooltip text for the string connecting the open and close trades of a position. The method is virtual and can be overridden in inherited classes if it is necessary to output slightly different data. When using the string returned by the method as a tooltip text, we should take into account the length of the string, as it is limited to approximately 160 characters for a tooltip, including control codes. Unfortunately, I cannot name the exact value as it was discovered empirically.
The method displaying deal labels on the chart:
//+------------------------------------------------------------------+ //| Display deal labels on the chart | //+------------------------------------------------------------------+ void CPosition::ShowDeals(const bool chart_redraw=false) { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.ShowArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
In a loop through the list of deals, get each successive deal and call the ShowArrow() method of the received object, which displays the deal label on the chart. At the end of the loop, redraw the chart if the appropriate flag is set.
The method hiding deal labels on the chart:
//+------------------------------------------------------------------+ //| Hide deal labels on the chart | //+------------------------------------------------------------------+ void CPosition::HideDeals(const bool chart_redraw=false) { for(int i=this.m_list_deals.Total()-1; i>=0; i--) { CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; deal.HideArrow(); } if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
In a loop through the list of deals, we get each successive deal and call the HideArrow() method of the received object, which hides the deal label from the chart. At the end of the loop, redraw the chart if the appropriate flag is set.
The method creating the line connecting open and close deals:
//+------------------------------------------------------------------+ //| Create a line connecting open-close deals | //+------------------------------------------------------------------+ bool CPosition::CreateLine(void) { //--- If the graphical line object could not be created, report this in the journal and return 'false' ::ResetLastError(); if(!::ObjectCreate(this.m_chart_id, this.m_line_name, OBJ_TREND, 0, 0, 0, 0, 0)) { ::Print("ObjectCreate() failed. Error ", ::GetLastError()); return false; } //--- Hide the line this.HideLine(); //--- Set the line to be drawn with dots, define the color and return 'true' ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_STYLE, STYLE_DOT); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, this.m_line_color); return true; }
A line is created with price and time coordinates at the zero position (price is 0, time is 01.01.1970 00:00:00). Hide the line from the chart and set the drawing style to dots and the default color.
Initially, the lines of each object are created hidden. At the moment they need to be displayed, the necessary coordinates and display are set for them.
The method that displays the connecting line between the deal labels:
//+------------------------------------------------------------------+ //| Display the connecting line between deal labels | //+------------------------------------------------------------------+ void CPosition::ShowLine(const bool chart_redraw=false) { //--- Get the pointers to the open and close deals CDeal *deal_in= this.GetDealIn(); CDeal *deal_out=this.GetDealOut(); //--- If no deals are received, leave if(deal_in==NULL || deal_out==NULL) return; //--- Set a start and end time, a price from the deal properties and a tooltip text for the line ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 0, deal_in.Time()); ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 1, deal_out.Time()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 0, deal_in.Price()); ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 1, deal_out.Price()); ::ObjectSetString(this.m_chart_id, this.m_line_name, OBJPROP_TOOLTIP, this.Tooltip()); //--- Show the line on the chart and update it if the flag is set ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
The method hiding the connecting line between the deal labels:
//+------------------------------------------------------------------+ //| Hide the connecting line between the deal labels | //+------------------------------------------------------------------+ void CPosition::HideLine(const bool chart_redraw=false) { ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
The visibility flag for the object is reset on all chart periods and, if the flag is set, the chart is updated.
The method setting the color of the connecting line:
//+------------------------------------------------------------------+ //| Set the color of the connecting line | //+------------------------------------------------------------------+ void CPosition::SetLineColor(const color clr=C'225,68,29') { if(::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, clr)) this.m_line_color=clr; }
The value passed to the method is set for the object as the color property. If all is successful, the color is set to the m_line_color variable.
The method setting Buy and Sell deal color:
//+------------------------------------------------------------------+ //| Set Buy and Sell deal color | //+------------------------------------------------------------------+ void CPosition::SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29') { //--- In the loop by the list of deals int total=this.m_list_deals.Total(); for(int i=0; i<total; i++) { //--- get the next deal object CDeal *deal=this.m_list_deals.At(i); if(deal==NULL) continue; //--- In case of Buy deal type, set a color for a Buy deal for the object if(deal.TypeDeal()==DEAL_TYPE_BUY) deal.SetColorArrow(clr_deal_buy); //--- In case of Sell deal type, set a color for a Sell deal for the object if(deal.TypeDeal()==DEAL_TYPE_SELL) deal.SetColorArrow(clr_deal_sell); } }
The method receives Buy and Sell deal colors. In the loop through the list of position deals, get the pointer to each successive deal and set the color for the deal label according to its type.
The method displaying a graphical representation of a position on a chart:
//+------------------------------------------------------------------+ //| Display a graphical representation of a position on a chart | //+------------------------------------------------------------------+ void CPosition::Show(const bool chart_redraw=false) { this.ShowDeals(false); this.ShowLine(chart_redraw); }
The method sequentially calls protected methods that display entry and exit deals and a line connecting them on the chart.
The method hiding a graphical representation of a position on a chart:
//+------------------------------------------------------------------+ //| Hide a graphical representation of a position on a chart | //+------------------------------------------------------------------+ void CPosition::Hide(const bool chart_redraw=false) { this.HideLine(false); this.HideDeals(chart_redraw); }
The method sequentially calls protected methods that hide entry and exit deals and a line connecting them on the chart.
The two methods considered above are public and are used to control the display of a position on the chart from the control program.
The method that prints the position properties and deals 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 is displayed with a brief description of the position and two lines with the time and price of opening and closing the position. Next, descriptions of all position deals from the list are printed in a loop.
As a result, we get the following data in the journal:
Position EURUSD 0.10 Sell #2523224572, Magic 0 -Opened 2024.05.31 17:06:15.134 [1.08734] -Closed 2024.05.31 17:33:17.772 [1.08639] Deal: Entry In 0.10 Sell #2497852906 at 2024.05.31 17:06:15.134 Deal: Entry Out 0.10 Buy #2497993663 at 2024.05.31 17:33:17.772
This is not very informative, but the Print() methods in these classes were made only for debugging.
Now we already have two classes: the deal class and the position class, which contains a list of deals that participated in the life of the position. The classes are simple and contain basic information on deals and some additional data on prices and time of opening/closing a position, as well as its profit in points. The remaining methods are used to obtain and display this information to a chart or text line.
Now let's create a general class, in which all positions will be collected from deals and placed in a list of historical positions. The class will provide access to the properties of positions and their deals, update and supplement the list of historical positions and display the specified position on the chart.
Historical position management class
In the same folder where we created the two previous classes, create a new file PositionsControl.mqh of the CPositionsControl class.
The class should be inherited from the CObject Standard Library base object, while the position class file is included into the CPositionsControl new class file:
//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { }
Declare the variables and methods for handling the class in the private, protected and public sections:
//+------------------------------------------------------------------+ //| Class of historical positions | //+------------------------------------------------------------------+ class CPositionsControl : public CObject { private: string m_symbol; // The symbol the position is open for long m_current_id; // ID of the current position displayed on the chart bool m_key_ctrl; // Flag for allowing to control the chart using the keyboard //--- Return the position type by deal type ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal); protected: CPosition m_temp_pos; // Temporary position object for searching CArrayObj m_list_pos; // List of positions long m_chart_id; // Chart ID //--- 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); //--- Return the pointer to the (1) first and the (2) last open position in the list CPosition *GetFirstClosedPosition(void); CPosition *GetLastClosedPosition(void); //--- Return the pointer to the (1) previous and (2) next closed position in the list CPosition *GetPrevClosedPosition(CPosition *current); CPosition *GetNextClosedPosition(CPosition *current); //--- Displays a graphical representation of the specified position on a chart void Show(CPosition *pos, const bool chart_redraw=false); //--- Center the chart on the currently selected position void CentersChartByCurrentSelected(void); //--- Return the ID of the current selected position long CurrentSelectedID(void) const { return this.m_current_id; } //--- Return the selected position (1) open and (2) close time datetime TimeOpenCurrentSelected(void); datetime TimeCloseCurrentSelected(void); //--- Hide the graphical representation of all positions on the chart except the specified one void HideAllExceptOne(const long pos_id, const bool chart_redraw=false); public: //--- Return the chart (1) symbol and (2) ID string Symbol(void) const { return this.m_symbol; } long ChartID(void) const { return this.m_chart_id; } //--- Create and update the list of positions. It can be redefined in the inherited classes virtual bool Refresh(void); //--- Return the number of positions in the list int Total(void) const { return this.m_list_pos.Total(); } //--- (1) Hide and (2) display the graphical representation of the first position on the chart void HideFirst(const bool chart_redraw=false); void ShowFirst(const bool chart_redraw=false); //--- (1) Hide and (2) display the graphical representation of the last position on the chart void HideLast(const bool chart_redraw=false); void ShowLast(const bool chart_redraw=false); //--- Display a graphical representation of the (1) current, (2) previous and (3) next position void ShowCurrent(const bool chart_redraw=false); void ShowPrev(const bool chart_redraw=false); void ShowNext(const bool chart_redraw=false); //--- Return the description of the currently selected position string CurrentSelectedDescription(void); //--- Print the properties of all positions and their deals in the journal void Print(void); //--- Constructor/destructor CPositionsControl(const string symbol=NULL); ~CPositionsControl(); };
Let's consider the declared methods in details.
Class constructor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPositionsControl::CPositionsControl(const string symbol=NULL) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); this.m_symbol = (symbol==NULL ? ::Symbol() : symbol); this.m_chart_id = ::ChartID(); this.m_current_id = 0; this.m_key_ctrl = ::ChartGetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL); }
Set the flag of sorting by close time in milliseconds for the list of historical positions. Set the symbol passed to the constructor into the variable. If this is an empty string, then it is the current symbol of the chart the program is running on. The chart ID is set as the current chart ID. It will be possible to set a different ID in the inherited classes. In the m_key_ctrl variable, set the flag allowing to control the chart using keys. After the program runs, this value will be set back to the chart property in order to return it to the state before the program was launched.
In the class destructor, the list of positions is destroyed, while the CHART_KEYBOARD_CONTROL chart property gets back the value that was present before the program launch:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPositionsControl::~CPositionsControl() { this.m_list_pos.Shutdown(); ::ChartSetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL, this.m_key_ctrl); }
The method that returns a 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(SORT_MODE_POSITION_IDENTIFIER); //--- Get the index of the position object with such an ID (or -1 if it is absent) from the list //--- Use the obtained index to get the pointer to the position 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); //--- Set 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(SORT_MODE_POSITION_TIME_CLOSE_MSC); return pos; }
The method logic is fully described in the code comments.
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; }
When creating a list of positions from a list of deals, it is important not to take into account the current market positions and avoid adding them to the list of historical positions. To understand that a position is not yet closed, we need to find it in the list of active positions by its ID. If such a position exists, there is no need to add it to the list. The method searches for positions from the list of market positions by their ticket, checks whether the position ID matches the value passed to the method, and if such a position exists (it can be selected), it returnstrue. Otherwise, it returns false.
The method returning the pointer to the first closed position in the list:
//+------------------------------------------------------------------+ //| Return the pointer to the first closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetFirstClosedPosition(void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); return this.m_list_pos.At(0); }
The list of positions is sorted by position closing time in milliseconds and a pointer to the first position (the oldest one) in the list is returned.
The method returning the pointer to the last closed position in the list:
//+------------------------------------------------------------------+ //| Return the pointer to the last closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetLastClosedPosition(void) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); return this.m_list_pos.At(this.m_list_pos.Total()-1); }
The list of positions is sorted by position closing time in milliseconds and a pointer to the last position in the list is returned.
The method returning the pointer to the previous closed position in the list:
//+------------------------------------------------------------------+ //| Return the pointer to the previous closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetPrevClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); int prev=this.m_list_pos.SearchLess(current); return this.m_list_pos.At(prev); }
Based on the currently selected position, the pointer to which is passed to the method, the previous one is searched for in the list, sorted by closing time in milliseconds. The SearchLess() method of the CArrayObj class returns the pointer to the first object encountered in the list that has the lower value, by which the list is sorted. In this case, the list is sorted by closing time in milliseconds. Accordingly, the first object found with a closing time less than the one passed to the method is the previous position. If the position object is not found, or the very first element of the list is passed to the method (there are no previous ones), then the method returns NULL.
The method returning the pointer to the next closed position in the list:
//+------------------------------------------------------------------+ //| Return the pointer to the next closed position in the list | //+------------------------------------------------------------------+ CPosition *CPositionsControl::GetNextClosedPosition(CPosition *current) { this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); int next=this.m_list_pos.SearchGreat(current); return this.m_list_pos.At(next); }
Based on the currently selected position, the pointer to which is passed to the method, the next one is searched for in the list, sorted by closing time in milliseconds. The SearchGreat() method of the CArrayObj class returns the pointer to the first object encountered in the list that has the higher value, by which the list is sorted. In this case, the list is sorted by closing time in milliseconds. Accordingly, the first object found with a closing time greater than the one passed to the method is the next position. If the position object is not found, or the very last element of the list is passed to the method (there are no next ones), the method returns NULL.
The method that displays a graphical representation of a given position on the chart:
//+----------------------------------------------------------------------------+ //| Display a graphical representation of the specified position on the chart | //+----------------------------------------------------------------------------+ void CPositionsControl::Show(CPosition *pos,const bool chart_redraw=false) { if(pos!=NULL) { pos.Show(chart_redraw); this.m_current_id=pos.ID(); } }
The method receives the pointer to the position whose graphical representation should be displayed on the chart. If a valid object is received, call its Show() method and add the position ID to the m_current_id variable. We are able to define the current selected position by the position ID in the m_current_id variable. A position is considered selected if its graphical representation is displayed on the chart.
The method hiding graphical representation of the first position from the chart:
//+------------------------------------------------------------------------+ //| Hide the graphical representation of the first position from the chart | //+------------------------------------------------------------------------+ void CPositionsControl::HideFirst(const bool chart_redraw=false) { CPosition *pos=this.GetFirstClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); }
Get the pointer to the first position in the list and call its Hide() method.
The method that displays a graphical representation of the first position on the chart:
//+---------------------------------------------------------------------------+ //| Display the graphical representation of the first position on the chart | //+---------------------------------------------------------------------------+ void CPositionsControl::ShowFirst(const bool chart_redraw=false) { //--- Get the pointer to the first closed position CPosition *pos=this.GetFirstClosedPosition(); if(pos==NULL) return; //--- Hide labels of all positions except the first one by its ID this.HideAllExceptOne(pos.ID()); //--- Display the labels of the first position on the chart and //--- center the chart by the labels of the currently selected position this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); }
Use the GetFirstClosedPosition() method considered above to get the pointer to the first position in the list, sorted by time in milliseconds. Hide all labels of all positions except the first one, display the first position and center the chart on the labels of its deals on the chart.
The method hiding graphical representation of the last position from the chart:
//+------------------------------------------------------------------+ //| Hide graphical representation of the last position from the chart| //+------------------------------------------------------------------+ void CPositionsControl::HideLast(const bool chart_redraw=false) { CPosition *pos=this.GetLastClosedPosition(); if(pos!=NULL) pos.Hide(chart_redraw); }
Get the pointer to the last position in the list and call its Hide() method.
The method that displays a graphical representation of the last position on the chart:
//+-----------------------------------------------------------------------+ //| Display the graphical representation of the last position on the chart| //+-----------------------------------------------------------------------+ void CPositionsControl::ShowLast(const bool chart_redraw=false) { //--- Get the pointer to the last closed position CPosition *pos=this.GetLastClosedPosition(); if(pos==NULL) return; //--- Hide labels of all positions except the last one by its ID this.HideAllExceptOne(pos.ID(), false); //--- Display the labels of the last position on the chart and //--- center the chart by the labels of the currently selected position this.Show(pos,chart_redraw); this.CentersChartByCurrentSelected(); }
Use the GetLastClosedPosition() method considered above to get the pointer to the last position in the list, sorted by time in milliseconds. Hide all labels of all positions except the last one, display the last position and center the chart on the labels of its deals on the chart.
The method that displays a graphical representation of the current position on the chart:
//+--------------------------------------------------------------------------+ //| Display a graphical representation of the current position on the chart | //+--------------------------------------------------------------------------+ void CPositionsControl::ShowCurrent(const bool chart_redraw=false) { //--- Get a pointer to the currently selected closed position CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); if(curr==NULL) return; //--- Display the labels of the current position on the chart and //--- center the chart by the labels of the currently selected position this.Show(curr,chart_redraw); this.CentersChartByCurrentSelected(); }
Get the pointer to a position whose ID is set in the m_current_id variable, display its graphical representation on the chart and center the chart by its deal labels on the chart.
The method that displays a graphical representation of the previous position on the chart:
//+------------------------------------------------------------------------+ //|Display a graphical representation of the previous position on the chart| //+------------------------------------------------------------------------+ void CPositionsControl::ShowPrev(const bool chart_redraw=false) { //--- Get the pointer to the current and previous positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *prev=this.GetPrevClosedPosition(curr); if(curr==NULL || prev==NULL) return; //--- Hide the current position, display the previous one and //--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(prev,chart_redraw); this.CentersChartByCurrentSelected(); }
Here we get the pointers to the currently selected and previous positions in the list. Hide the labels of the current position and display the labels of the previous one. When they are displayed, the position ID is set to the m_current_id variable indicating that this position is now the current one. Use its labels to center the chart.
The method displaying a graphical representation of the next position on the chart:
//+---------------------------------------------------------------------+ //| Display a graphical representation of the next position on the chart| //+---------------------------------------------------------------------+ void CPositionsControl::ShowNext(const bool chart_redraw=false) { //--- Get the pointer to the current and next positions CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID()); CPosition *next=this.GetNextClosedPosition(curr); if(curr==NULL || next==NULL) return; //--- Hide the current position, display the next one and //--- center the chart by the labels of the currently selected position curr.Hide(); this.Show(next,chart_redraw); this.CentersChartByCurrentSelected(); }
The method is identical to the previous one, except that here we get the pointers to the currently selected and next positions in the list. Hide the labels of the current position and display the icons of the next one. When they are displayed, the position ID is set to the m_current_id variable indicating that this position is now the current one. Use its labels to center the chart.
The method hiding the graphical representation of all positions on the chart except the specified one:
//+------------------------------------------------------------------+ //| Hide the graphical representation | //| of all positions except the specified one | //+------------------------------------------------------------------+ void CPositionsControl::HideAllExceptOne(const long pos_id,const bool chart_redraw=false) { //--- In a loop by the list of positions int total=this.m_list_pos.Total(); for(int i=0; i<total; i++) { //--- get the pointer to the next position and CPosition *pos=this.m_list_pos.At(i); if(pos==NULL || pos.ID()==pos_id) continue; //--- hide the graphical representation of the position pos.Hide(); } //--- After the loop, update the chart if the flag is set if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
The method receives the ID of the position whose labels need to be left on the chart. The labels for all other positions are hidden.
The method that centers the chart on the currently selected position:
//+------------------------------------------------------------------+ //| Center the chart at the currently selected position | //+------------------------------------------------------------------+ void CPositionsControl::CentersChartByCurrentSelected(void) { //--- Get the index of the first visible bar on the chart and the number of visible bars int bar_open=0, bar_close=0; int first_visible=(int)::ChartGetInteger(this.m_chart_id, CHART_FIRST_VISIBLE_BAR); int visible_bars =(int)::ChartGetInteger(this.m_chart_id, CHART_VISIBLE_BARS); //--- Get the position opening time and use it to get the opening bar datetime time_open=this.TimeOpenCurrentSelected(); if(time_open!=0) bar_open=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_open); //--- Get the position opening time and use it to get the closing bar datetime time_close=this.TimeCloseCurrentSelected(); if(time_close!=0) bar_close=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_close); //--- Calculate the width of the window the deal labels are located in int width=bar_open-bar_close; //--- Calculate the chart offset so that the window with deals is in the center of the chart int shift=(bar_open + visible_bars/2 - width/2); //--- If the window width is greater than the chart width, the opening deal is located on the second visible bar if(shift-bar_open<0) shift=bar_open+1; //--- If the deal opening bar is to the left of the first visible bar of the chart //--- or the deal opening bar is to the right of the chart last visible bar, //--- scroll the chart by the calculated offset if(bar_open>first_visible || bar_open<first_visible+visible_bars) ::ChartNavigate(this.m_chart_id, CHART_CURRENT_POS, first_visible-shift); }
The entire logic of the method is fully described in the comments to the code. The method shifts the symbol chart so that visually all position deals with the connecting line are located in the center of the chart. If the labels of all deals do not fit within the chart width, the chart is shifted so that the position opening deal is on the second visible bar of the chart from the left.
The method that returns the opening time of the currently selected position:
//+------------------------------------------------------------------+ //| Return the opening time of the currently selected position | //+------------------------------------------------------------------+ datetime CPositionsControl::TimeOpenCurrentSelected(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.Time() : 0); }
Get the pointer to the current selected position by its ID set in the m_current_id variable. If the pointer is successfully received, return the position open time. Otherwise, return zero.
The method returning the closing time of the currently selected position:
//+------------------------------------------------------------------+ //| Return the current selected position closing time | //+------------------------------------------------------------------+ datetime CPositionsControl::TimeCloseCurrentSelected(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.TimeClose() : 0); }
Get the pointer to the current selected position by its ID set in the m_current_id variable. If the pointer is successfully received, return the position close time. Otherwise, return zero.
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; } }
The method receives the pointer to the deal. Depending on the deal type, return the appropriate position type.
The method creating 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(SORT_MODE_POSITION_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, or if the deal is not for the symbol set for the class, 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) || ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol) 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 pos=new CPosition(pos_id, this.m_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.SetTime(deal.Time()); pos.SetTimeMsc(deal.TimeMsc()); ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetPriceOpen(deal.Price()); pos.SetVolume(deal.Volume()); } if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY) { pos.SetPriceCurrent(deal.Price()); } if(deal.Entry()==DEAL_ENTRY_INOUT) { ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal); pos.SetTypePosition(type); pos.SetVolume(deal.Volume()-pos.Volume()); } } //--- Set the flag of sorting by close time in milliseconds for the position list this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC); //--- Return the result of creating and adding a position to the list return res; }
The method logic has been described in the code comments in detail.
Let's have a cursory look at the logic of searching for historical positions: the client terminal only contains a list of current positions. Each position has its own deals located in the list of historical deals. Each deal contains an ID of the position it participated in. This ID can be used to determine a position a deal belongs to. Accordingly, to create a list of historical positions, we need to loop through the list of historical deals and use the ID to determine the position the deal belongs to. Be sure to check that a position with such an ID is not in the list of active positions. If it is, this is not yet a closed position, and the deal should be skipped. If such a position is no longer in the market, then we need to create a new historical position object with such an ID and add this deal to the list of deals of the created position. Before creating a position object, we need to check whether such a position has been created before. If yes, then there is no need to create a position object. Instead, simply add this deal to the list of the already created position. Of course, if such a deal is already in the position list, then there is no need to add it. After completing the cycle of historical transactions, a list of already closed positions will be created as a result, and each position in this list will contain a list of its deals.
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(); } }
Starting from the earliest position (from the very beginning of the list), we get each successive position and output its description to the journal in a loop.
The method returning the description of each selected position:
//+------------------------------------------------------------------+ //| Return the description of the currently selected position | //+------------------------------------------------------------------+ string CPositionsControl::CurrentSelectedDescription(void) { CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID()); return(pos!=NULL ? pos.Tooltip() : NULL); }
Get the pointer to the current selected position whose ID is set in the m_current_id variable and return the string created to display the tooltip. This string can be used to display it in a chart comment. Let's show it in the test EA for a visual display of some properties of the position whose labels are displayed on the chart. The test EA will contain the following functionality:
- When launched, a trading history is created in the form of a list of historical positions. Each position contains the list of its deals;
- Once the list of historical positions is complete, the last closed position is displayed on the chart as open and close labels connected by the line;
- You can navigate through the list of historical positions using the cursor keys while holding down the Ctrl key:
- Left key - display the previous position on the chart;
- Right key - display the next position on the chart;
- Up key - display the first position on the chart;
- Down key - display the last position on the chart;
- Hold down the Shift key to display a comment on the chart describing the currently selected position in the list.
Test
In \MQL5\Experts\PositionsViewer\, create a new EA file PositionViewer.mq5.
Include the file of historical position management class, declare macro substitutions for the key codes and EA global variables:
//+------------------------------------------------------------------+ //| PositionViewer.mq5 | //| 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 <PositionsViewer\PositionsControl.mqh> #define KEY_LEFT 37 #define KEY_RIGHT 39 #define KEY_UP 38 #define KEY_DOWN 40 //--- global variables CPositionsControl ExtPositions; // Historical position class instance bool ExtChartScroll; // Chart scrolling flag bool ExtChartHistory; // Trading history display flag //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+
In the EA OnInit() handler, set the following code:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Save the chart auto scroll flag and disable auto scroll ExtChartScroll=ChartGetInteger(ChartID(), CHART_AUTOSCROLL); ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false); //--- Save the trading history display flag and disable history display ExtChartHistory=ChartGetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY); ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false); //--- Create a list of closed positions and display the list creation time in the journal ulong start=GetTickCount64(); Print("Reading trade history and creating a list of historical positions"); ExtPositions.Refresh(); ulong msec=GetTickCount64()-start; PrintFormat("List of historical positions created in %I64u msec", msec); //ExtPositions.Print(); //--- If this is a launch after changing the chart period, display the currently selected position if(UninitializeReason()==REASON_CHARTCHANGE) ExtPositions.ShowCurrent(true); //--- otherwise, display the last closed position else ExtPositions.ShowLast(true); //--- Successful return(INIT_SUCCEEDED); }
The flags of auto scrolling the chart and displaying the trading history are saved here in order to restore them when the program is closed. Chart auto scroll and history display are disabled, a list of all historical positions that ever existed on the current symbol is created, and the time of list creation is displayed in the journal. If we uncomment the string //ExtPositions.Print();, after creating the list of closed positions, all historical positions from the created list will be displayed in the journal. If this is not a change of the chart timeframe, the labels of the last closed position are drawn on the chart, and the chart is scrolled so that the labels are in its center. If this is a change of the chart period, then the currently selected position in the list is displayed.
In the OnDeinit() handler, restore the saved values of the chart auto scroll and trading history display, and remove comments from the chart:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Restore the auto scroll and trading history property initial value and remove chart comments ChartSetInteger(ChartID(), CHART_AUTOSCROLL, ExtChartScroll); ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, ExtChartHistory); Comment(""); }
In the OnTradeTransaction() handler, call the method of updating the list of the historical positions control class each time a new deal occurs. If the position is closed, display its labels on the chart:
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- In case of a transaction type, add a new deal if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { //--- update the list of positions and their deals ExtPositions.Refresh(); //--- Get the new deal ticket ulong deal_ticket=trans.deal; //--- If the ticket is not received or failed to get the method for updating the position from the deal properties, leave long entry; if(deal_ticket==0 || !HistoryDealGetInteger(deal_ticket, DEAL_ENTRY, entry)) return; //--- If this is an exit deal, display the last closed position on the chart if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY) ExtPositions.ShowLast(true); } }
In the event handler, track key pressing events and respond to cursor keys pressed while holding Ctrl to navigate through the list of closed positions. Also, respond to the Shift key to display a position description in the chart comment, as well as respond to changing the chart scale to center the chart by the current position labels:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If the event ID is pressing a key if(id==CHARTEVENT_KEYDOWN) { //--- If the Ctrl key is held down if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0) { //--- If the chart scrolling with keys is active, disable it if((bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); //--- If the left key is pressed, display the previous closed position if(lparam==KEY_LEFT) ExtPositions.ShowPrev(true); //--- If the right key is pressed, display the next closed position if(lparam==KEY_RIGHT) ExtPositions.ShowNext(true); //--- If the up key is pressed, display the first closed position if(lparam==KEY_UP) ExtPositions.ShowFirst(true); //--- If the down key is pressed, display the last closed position if(lparam==KEY_DOWN) ExtPositions.ShowLast(true); } //--- If Ctrl is not pressed, else { //--- If the chart scrolling with keys is inactive, enable it if(!(bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL)) ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true); } } //--- If the Shift key is held down, display a description of the current position in the chart comment if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0) Comment(ExtPositions.CurrentSelectedDescription()); //--- If the Shift key is not pressed, check the comment on the chart and delete it if it is not empty else { if(ChartGetString(ChartID(),CHART_COMMENT)!=NULL) Comment(""); } //--- If the horizontal scale of the chart has changed, display the currently selected position static int scale=-1; if(id==CHARTEVENT_CHART_CHANGE) { int scale_curr=(int)ChartGetInteger(ChartID(), CHART_SCALE); if(scale!=scale_curr) { ExtPositions.ShowCurrent(true); scale=scale_curr; } } }
This is the arrangement of closed positions with a standard display of trading history:
This is a good illustration of what was written at the very beginning of the article:
Now let's compile the EA and launch it on the chart:
At launch, the history displayed by standard means was hidden with only the last closed position remaining visible. We can navigate through the history of positions by holding down Ctrl and pressing the cursor keys. While moving through the list of closed positions, we can easily see a graphical representation of the trading history in the place where the chart was filled with labels of all the deals of the positions closed here.
If we also hold down Shift, a description of the currently selected closed position is displayed in the chart comment.
Now let's find a position whose transactions are located at a sufficient distance from each other, and see how they are centered on the chart if they do not fit in one visible area of the chart window when its horizontal scale is increased:
We see that if both deals do not fit into the chart window, it is centered so that the deal entering the position on the second bar from the left is visible.
If we press the Shift key, we will see a description of this closed position in the chart comment:
This is convenient because it allows us to see a description of the position without hovering the mouse cursor over the line connecting the deals. If we move through the list of closed positions using the cursor keys, holding down Ctrl + Shift, we can clearly see the description of the closed position currently displayed on the chart.
Launching the EA starts creation of the list of historical positions. In my case, with a short history from 2023, the time of the list creation:
PositionViewer (EURUSD,M15) Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M15) List of historical positions created in 6422 msec
Subsequent chart period switching no longer requires time to re-create the list:
PositionViewer (EURUSD,M1) Reading trade history and creating a list of historical positions PositionViewer (EURUSD,M1) List of historical positions created in 31 msec PositionViewer (EURUSD,M5) Reading trade history and creating a list of historical positions PositionViewer (EURUSD,M5) List of historical positions created in 47 msec PositionViewer (EURUSD,M1) Reading trade history and creating a list of historical positions PositionViewer (EURUSD,M1) List of historical positions created in 31 msec
Conclusion
The trading history provided by the created program is more convenient in case of active and frequent trading - the chart is never overloaded with labels of closed positions - each position is displayed independently, and switching between displaying the current and next or previous position is done with the cursor keys. Each deal label has more information in its tooltip than the standard trading history. Hovering over the line connecting the deals will display a tooltip with information about the closed position.
The classes created in the article can be used in our own developments, expand functionality and create more complex and functional programs with information on trading history and the history of closed positions.
All class files and test EA are attached to the article and are available for independent study. The MQL5.zip archive contains files in such a way that they can be immediately unzipped into the MQL5 terminal directory, and the new PositionsViewer\ folder will be created in the Experts folder along with all the project files: the PositionViewer.mq5 EA file and three included class files. We are free to just compile the EA file and use it.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/15026





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use