Analyzing Balance/Equity graphs by symbols and EAs' ORDER_MAGIC

Vladimir Karputov | 14 June, 2017

Contents


Setting a task

With the introduction of hedging, MetaTrader 5 provides an excellent opportunity to trade several Expert Advisors (or strategies) on a single trading account simultaneously. The entire trading account can be tracked in the Signals service with the ability to receive the statistical data. However, one important issue remains: How to visualize the contribution of each strategy on the Balance and Equity graphs?

It is not uncommon in trading when one strategy is profitable, while the second one is loss-making. As a result, the overall result may hang around zero. In this case, it is useful to build the Balance and Equity graphs for each trading strategy separately.


1. Commission. Swap. Profit

The total financial trade result is defined by summing the three parameters:

 Result=Deal commission +Cumulative swap on close+ Deal profit

These trade properties are received using HistoryDealGetDouble() with the following IDs:

DEAL_COMMISSION

Deal commission

double

DEAL_SWAP

Cumulative swap on close

double

DEAL_PROFIT

Deal profit

double


Example of receiving trade properties from the trading history within a specified time interval in the HistoryDealGetTicket.mq5 script.

The script operation results (excluding trades of DEAL_ENTRY_IN type since they have no financial result):

...
  4: deal #36774600 at 2017.02.15 10:17:50 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 1.52 NZDUSD.m (order #47802989, position ID 47770449)
...
12: deal #36798157 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.01 profit: 2.98 EURUSD.m (order #47827771, position ID 47685190)
13: deal #36798161 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.02 profit: 5.99 EURUSD.m (order #47827785, position ID 47665575)
14: deal #36798176 at 2017.02.15 16:44:17 Entry out, buy vol: 0.01 comm: 0 swap: -0.02 profit: 5.93 EURUSD.m (order #47827805, position ID 47605603)
15: deal #36798185 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.03 profit: 5.98 EURUSD.m (order #47827821, position ID 47502789)
16: deal #36798196 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.03 profit: 8.96 EURUSD.m (order #47827832, position ID 47419515)
17: deal #36798203 at 2017.02.15 16:44:18 Entry out, buy vol: 0.01 comm: 0 swap: -0.06 profit: 8.92 EURUSD.m (order #47827835, position ID 47130461)
18: deal #36798212 at 2017.02.15 16:44:19 Entry out, sell vol: 0.01 comm: 0 swap: -0.48 profit: -21.07 EURUSD.m (order #47827845, position ID 46868574)
...
25: deal #36824799 at 2017.02.15 19:57:57 Entry out, sell vol: 0.01 comm: 0 swap: 0 profit: 2.96 NZDUSD.m (order #47855548, position ID 47817757)
26: deal #36824800 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0 profit: 3.01 NZDUSD.m (order #47855549, position ID 47790966)
27: deal #36824801 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 3.07 NZDUSD.m (order #47855550, position ID 47777495)
28: deal #36824802 at 2017.02.15 19:57:58 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 3 NZDUSD.m (order #47855551, position ID 47759307)
29: deal #36824803 at 2017.02.15 19:57:59 Entry out, sell vol: 0.01 comm: 0 swap: 0.02 profit: 1.52 NZDUSD.m (order #47855552, position ID 47682775)
...
33: deal #36832775 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 2.96 NZDUSD.m (order #47863883, position ID 47826616)
34: deal #36832776 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 3.05 NZDUSD.m (order #47863884, position ID 47803010)
35: deal #36832777 at 2017.02.16 00:58:41 Entry out, sell vol: 0.01 comm: 0 swap: 0.05 profit: 2.98 NZDUSD.m (order #47863885, position ID 47792294)
36: deal #36832778 at 2017.02.16 00:58:42 Entry out, sell vol: 0.01 comm: 0 swap: 0.07 profit: 2.88 NZDUSD.m (order #47863886, position ID 47713741)
...

As we can see, swap and profit can have either "+" or "-" sign. Therefore, addition is used in the total financial result equation for swap and profit.


2. Calculating equity and balance on history

The general operation principle includes forming the list of "Open positions". The trading history is divided into segments 5-15 minutes each (this parameter will be defined more precisely later). Then we should do the following for each segment successively:

2.1. "Open positions" list

We need a deal class to consider open positions on a selected segment. Let's implement it in the CHistoryDeal class — in the HistoryDeal.mqh include file:

Method Value
TicketDeal Get/set "Deal ticket" property
PosIDDeal Get/set "Position ID" property
SymbolDeal Get/set "Deal symbol" property
TypeDeal Get/set "Deal type" property from the ENUM_DEAL_TYPE enumeration
EntryDeal Get/set "Trade direction" property from the ENUM_DEAL_ENTRY enumeration
VolumeDeal Get/set "Trade volume property"
PriceDeal Get/set "Trade open price" property


2.2. Floating profit equation

Profit equations are taken from the ENUM_SYMBOL_CALC_MODE enumeration description. While doing so, always keep in mind the currency, in which the profit for the symbol is calculated. The profit currency can be obtained:

SymbolInfoString(m_name,SYMBOL_CURRENCY_BASE);

rts specification

Fig. 1. RTS-3.17 specification

2.3. Avoiding issues: Are all symbols available?

Possible issues: 

To take into account possible errors in the program's (not terminal's!) global variables, we introduce the array for "bad" symbols not present in the "Market Watch", as well as the ones, for which it is impossible to re-calculate the profit currency into a deposit one:

string m_arr_defective_symbols[];                        // array for defective symbols

The sequence of passing the check (all stages pass in the SearchDefectiveSymbols function):

  1. create a temporary (auxiliary) array of all symbols present in the "Market Watch";
  2. create a temporary (auxiliary) array of all trading history symbols (in the specified segment of dates from ... to ... );
  3. looking for symbols from the trading history in the "Market Watch". If any of the symbols is not found, it is sent to the list of "bad" symbols;
  4. symbols from trading history are added (displayed) in the "Market Watch". If the operation fails, the symbol is sent to the list of "bad" symbols;
  5. get the profit currency from the symbol properties. If the symbol profit currency is different from the deposit one, move to 5.1 sub-step.
    1. Trying to find a "Market Watch" symbol for re-calculation. The symbol should correspond to "deposit currency"+"profit currency" or "profit currency"+"deposit currency". If the operation fails, the symbol is sent to the list of "bad" symbols

After the "SearchDefectiveSymbols" function is complete, the array with "bad" symbols from the trading history is formed. "Bad" symbols are not used in further calculations (of the Balance and Equity graphs).

2.4. Considering opened and closed positions on history

All balance changes are displayed in the m_arr_balance_equity two-dimensional array.

Request the history within the "from date" and "to date" dates (EA inputs) and divide it by the number of cycles having the duration of "timer (minutes)". The work is done in the "CreateBalanceEquity" function. All trades are written in the "ArrOpenPositions" dynamic pointers array. The subsequent work with the array is adding and removing deals.

Depending on the "Deal direction" parameter — ENUM_DEAL_ENTRY, the Balance and Equity charts are calculated differently. If you want to check the behavior during opening, closing, partial closing or reversing positions on hedging and netting accounts, you can launch the HistorySelect.mq5 script.

2.5. How to count the floating profit (Equity)

It is necessary to go along the entire ArrOpenPositions array and calculate a floating profit for all trades in the array. If a trade is in the ArrOpenPositions array, we have certain data on each trade with the most important being as follows:

and a final date the trading history is requested for. The only data missing are the symbol prices of the trade as of the final date, up to which the trading history is requested. Prices are received using CopyTicks in CalculationFloatingProfit. As already mentioned, the profit equations are taken from the ENUM_SYMBOL_CALC_MODE enumeration description. 

During profiling, it turned out that the CopyTicks method is very time consuming, especially when the CalculationFloatingProfit array contains multiple trades at one symbol. In such cases, the same data are requested several times, since a symbol and a date are the same. We have implemented caching of results (CopyTicks) to accelerate the process: 

//--- caching results "CopyTicks"
   static string arr_name[];
   static double arr_ask[];
   static double arr_bid[];
   static datetime prev_time=0;
   int number=-1;
   if(time>prev_time)
     {
      prev_time=time;
      ArrayFree(arr_name);
      ArrayFree(arr_ask);
      ArrayFree(arr_bid);
     }
   found=false;
   size=ArraySize(arr_name);
   if(size>0)
      for(int i=0;i<size;i++)
        {
         if(name==arr_name[i])
           {
            number=i;
            found=true;
            break;
           }
        }
   if(found)
     {
      ArrayResize(ticks_array,1);
      ticks_array[0].ask=arr_ask[number];
      ticks_array[0].bid=arr_bid[number];
     }
   else
     {
      //---
      int copy_ticks=-1;
      int count=0;
      while(copy_ticks==-1 && count<5)
        {
         copy_ticks=CopyTicks(name,ticks_array,COPY_TICKS_INFO,1000*(ulong)time,1);
         //if(copy_ticks==-1)

Bid and ask results are saved to local arrays: if ticks have already been received for a symbol within that time interval, these ticks are simply taken from the local arrays. This approach accelerates the process 10-15 times. 


3. Integrating the EA to the dialog box (panel)

Now that the EA's basics are ready, it is time to make the interface more user-friendly. Considering the article's name "Analyzing Balance/Equity graphs by symbols and EAs' ORDER_MAGIC", it is necessary to provide several filtration features:

We already have the m_arr_all_trade_symbols array to consider all trade symbols. It is declared on the global program level. Let's introduce yet another array m_arr_all_magics for considering all magic numbers. To do this, upgrade the FillArrayTradeSymbols function: now, the m_arr_all_magics array is to be filled in it as well.

3.1. General view of the dialog box

panel

Fig. 2. General view of the panel

After forming the arrays of "bad" symbols, all trading symbols and magic numbers, the two lists are filled in the panel (elements based on the CComboBox class): the left list is filled with all trade symbols, the right one is filled with all magic numbers. Selecting all symbols and magic numbers comes first in the lists:

panel combo box

Fig. 3. Dropdown lists

The panel EA operation logic is as follows: only after pressing the Start button, the system checks what has been selected in the two dropdown lists. Depending on the selected parameters, the Balance/Equity graphs are plotted in the lists based on the trading history.

3.2. Interacting with the box

I decided that transferring the entire EA code to the panel class (APHDialog.mqh) would take too much time. The alternative solution: introducing the m_ready, m_symbol and m_magic internal variables to the panel class:

//+------------------------------------------------------------------+
//| Class CAPHDialog                                                 |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CAPHDialog : public CAppDialog
  {
private:
   CLabel            m_label_symbols;                 // the label object
   CComboBox         m_combo_box_symbols;             // the combo box object
   CLabel            m_label_magics;                  // the label object
   CComboBox         m_combo_box_magics;              // the combo box object
   CButton           m_button_start;                  // the button object
   //---
   bool              m_ready;                         // true -> you can build graphics
   string            m_symbol;
   ulong             m_magic;

public:

Decision on the m_ready variable status is made when processing the Start button click:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnClickButtonStart(void)
  {
   if(m_combo_box_symbols.Select()!=" " && m_combo_box_magics.Select()!=" ")
      m_ready=true;
   else
      m_ready=false;
//Comment(__FUNCTION__+" ButtonStartclick"+"\n"+
//        "Symbols: "+"\""+m_combo_box_symbols.Select()+"\""+"\n"+
//        "Magic: "+"\""+m_combo_box_magics.Select()+"\""+"\n"+
//        "m_ready: "+IntegerToString(m_ready));
  }

Note the highlighted string: right after creating and filling the dropdown lists (these are the m_combo_box_symbols and m_combo_box_magics elements in our case), the element with the " " value (space) is set in the lists.

Decision on the m_symbol and m_magic variables states is made when processing the click on the appropriate dropdown list:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnChangeComboBoxSymbols(void)
  {
//Comment(__FUNCTION__+" \""+m_combo_box_symbols.Select()+"\"");
   if(m_combo_box_symbols.Select()=="All symbols")
      m_symbol="";
   else
      m_symbol=m_combo_box_symbols.Select();
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CAPHDialog::OnChangeComboBoxMagics(void)
  {
//Comment(__FUNCTION__+" \""+m_combo_box_magics.Select()+"\"");
   if(m_combo_box_magics.Select()=="All magics")
      m_magic=-1;
   else
      m_magic=StringToInteger(m_combo_box_magics.Select());
  }

Thus, the three variables m_ready, m_symbol and m_magic are filled when clicking the Start button. Now, all that is left to do is inform the EA that parameters have been selected in the panel. The solution is simple: in the EA, we should start the timer with the interval of three seconds. The timer is to survey the panel. To do this, let's write the CAPHDialog::IsReady method in the panel.

//+------------------------------------------------------------------+
//| On the panel there are chosen parameters                         |
//+------------------------------------------------------------------+
bool CAPHDialog::IsReady(string &symbol,ulong &magic)
  {
   if(m_ready)
     {
      symbol=m_symbol;
      magic=m_magic;
      m_ready=false;
      return(true);
     }
   else
      return(false);
  }

In this method, we will write the values of internal variables to the variables passed by reference and reset the m_ready internal variable.

3.3. Small adjustment - considering selected magic number

Trades are selected according to the specified conditions: by a symbol or by all symbols and magic or by all magic numbers in GetHistory:

//--- for all deals 
   for(int i=0;i<deals;i++)
     {
      deal_ticket          = HistoryDealGetTicket(i);
      deal_position_ID     = HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
      deal_symbol          = HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
      deal_type            = (ENUM_DEAL_TYPE)HistoryDealGetInteger(deal_ticket,DEAL_TYPE);
      deal_entry           = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);
      deal_volume          = HistoryDealGetDouble(deal_ticket,DEAL_VOLUME);
      deal_price           = HistoryDealGetDouble(deal_ticket,DEAL_PRICE);
      deal_commission      = HistoryDealGetDouble(deal_ticket,DEAL_COMMISSION);
      deal_swap            = HistoryDealGetDouble(deal_ticket,DEAL_SWAP);
      deal_profit          = HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
      deal_magic           = HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
      if(sSymbol!="")
         if(deal_symbol!=sSymbol)
            continue;
      if(uMagic!=ULONG_MAX)
         if(deal_magic!=uMagic)
            continue;
      //--- onle BUY or SELL

Note that for the uMagic variable, ULONG_MAX means "all magics", while for the sSymbol variable, "" means "All symbols".

Plotting the balance and floating profit based on the trading history (first, for all symbols, then — only for one):


Video


4. MFE and MAE distribution graphs

Maximum profit (MFE) and maximum loss (MAE) values are recorded for each open order during its lifetime. These parameters additionally characterize each closed order using the values of the maximum unrealized potential and maximum permitted risk. MFE/Profit and MAE/Profit distribution graphs display each order as a point with received profit/loss value plotted along the X-axis, while maximum displayed values of potential profit (MFE) and potential loss (MAE) are plotted along the Y-axis.

4.1. MFE and MAE accounting principle

The m_arr_pos_id array for positions accounting and the m_arr_mfe_mae array for considering MFE, total financial result and MAE are declared on the global program level: 

long   m_arr_pos_id[];
double m_arr_mfe_mae[][3];  // [][0] - mfe, [][1] - final financial result,[][2] - mae

Subsequently, the MFE and MAE distribution point graphs are plotted based on the m_arr_mfe_mae array.

The m_arr_pos_id array for positions accounting and the m_arr_mfe_mae one for considering MFE always have one size in the first dimension — thus, the "i" position (m_arr_pos_id[i]) always has a clear match m_arr_mfe_mae[i][][][]. The sizes of these two arrays are set in GetHistory:

         if(deal_symbol=="")
            DebugBreak();
         ArrOpenPositions.Add(HistoryDeal);
         //--- mfe, mae
         int size=ArraySize(m_arr_pos_id);
         ArrayResize(m_arr_pos_id,size+1,10);
         ArrayResize(m_arr_mfe_mae,size+1,10);
         m_arr_pos_id[size]=deal_position_ID;
         // [][0] - mfe, [][1] - final financial result,[][2] - mae
         m_arr_mfe_mae[size][0]=0.0;
         m_arr_mfe_mae[size][1]=0.0;
         m_arr_mfe_mae[size][2]=0.0;
         continue;
        }
      //--- 
      if(deal_entry==DEAL_ENTRY_OUT)

The function that is responsible for recording the maximum profit and loss for each position:

//+------------------------------------------------------------------+
//| Add Result Mfe Mae                                               |
//+------------------------------------------------------------------+
void AddResultMfeMae(const long pos_id,const double floating_profit,const double financial_result)
  {
// [][0] - mfe (profit), [][1] - final financial result,[][2] - mae (loss)
//--- search pos_id
   int position=-1;
   int size=ArraySize(m_arr_pos_id);
   for(int i=0;i<size;i++)
      if(m_arr_pos_id[i]==pos_id)
        {
         position=i;
         break;
        }
   if(position==-1)
      return;

//---
   if(floating_profit==0.0)
      return;

   if(floating_profit>0.0) // profit
     {
      if(m_arr_mfe_mae[position][0]<floating_profit)
         m_arr_mfe_mae[position][0]=floating_profit;
     }
   else // loss
     {
      if(m_arr_mfe_mae[position][2]>floating_profit)
         m_arr_mfe_mae[position][2]=floating_profit;
     }
   m_arr_mfe_mae[position][1]=financial_result;
  }

All three parameters should be passed. In other words, if our virtual position is still in the ArrOpenPositions list of open positions and its floating profit is -20.2, the call looks as follows:

    AddResultMfeMae(pos_id,-20.2,0.0);

if our virtual position is still in the ArrOpenPositions list of open positions and its floating profit is +5.81, the call looks as follows:

    AddResultMfeMae(pos_id,5.81,0.0);

if our virtual position is removed from the ArrOpenPositions list of open positions and its total financial result is -3.06, the call looks as follows:

    AddResultMfeMae(pos_id,0.0,-3.06);

In other words, if there is a floating profit, the total financial result is 0. If the position is closed, the floating profit is equal to 0.

4.2. Processing positions

Besides, in GetHistory, write [floating profit][total financial result 0.0] to the m_arr_mfe_mae array for each position if you want to re-calculate a floating profit.

4.3. Changed box

In order to display both Balance/Equity and MFE/MAE graphs, the box should be slightly changed:

panel 2

Fig. 4. Changed box

MFE (maximum profit) and MAE (maximum loss) are built using the two coordinates: X - position's total financial result, Y - MFE or MAE value, respectively.

MFE

Fig. 5. MFE

MAE

Fig. 6. MAE


Conclusion

Now, you can see the balance and equity statistics per each symbol and magic number on hedge accounts when applying several EAs simultaneously. In other words, you are able to visually define the contribution of each EA (ORDER_MAGIC) to the overall balance, as well as each EA's drawdown.

Programs used in the article:

#
 Name
Type
Description
1
HistoryDeal.mqh Library  Class for considering open positions on a selected segment
2
HistoryDealGetTicket.mq5 EA  Example of receiving trade properties from the trading history within a specified time interval
3
APHDialog.mqh Library  EA dialog box class
4
Accounting_positions_on_history.mq5 EA  The main EA described in the article
5
MQL5.zip Archive  The archive containing the main EA and its include files.