Serious bug in the OrderCalcProfit() function

amrali  

According to the documentation of OrderCalcProfit"The function calculates the profit for the current account, in the current market conditions, based on the parameters passed. The function is used for pre-evaluation of the result of a trade operation. The value is returned in the account currency."

But, the flaw is that it, I noticed that it calculates the profit of ORDER_TYPE_BUY orders, regardless profitable or not, using the value of SYMBOL_TRADE_TICK_VALUE_LOSS. And, for the ORDER_TYPE_SELL orders, regardless profitable or not, it calculates with the value of SYMBOL_TRADE_TICK_VALUE_PROFIT. I think this is a serious bug in an important trading function in the platform.

The proper logic is to calculate the profit/loss using the SYMBOL_TRADE_TICK_VALUE_XXXXX, that matches the outcome of the order. 

#define PRINT(A) Print(#A + " = ", (A))
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   string symbol = "AUDCAD";   // any symbol whose profit currency != account currency.
   double volume = 1e11;       // increase precision from 2 places (rounding done by OrderCalcProfit) to 13 places (11 + 2).
   double profit;
   double price = SymbolInfoDouble(symbol, SYMBOL_BID);    // try any price, even 1.11 or 3.33 will do.
   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);

//--- try buy and sell trades, each profitable and losing.
   Print("Buy orders:");
   if(OrderCalcProfit(ORDER_TYPE_BUY, symbol, volume, price, (price + ticksize), profit))  {  PRINT(profit/volume);  }
   if(OrderCalcProfit(ORDER_TYPE_BUY, symbol, volume, price, (price - ticksize), profit))  {  PRINT(-profit/volume); }

   Print("");
   Print("Sell orders:");
   if(OrderCalcProfit(ORDER_TYPE_SELL, symbol, volume, price, (price - ticksize), profit))  {  PRINT(profit/volume);  }
   if(OrderCalcProfit(ORDER_TYPE_SELL, symbol, volume, price, (price + ticksize), profit))  {  PRINT(-profit/volume); }

//--- surprise!
   Print("");
   PRINT(DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_LOSS),13));
   PRINT(DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT),13));
  }
//+------------------------------------------------------------------+

// sample output:
/*
 Buy orders:
 profit/volume = 0.6990074094785
 -profit/volume = 0.6990074094785

 Sell orders:
 profit/volume = 0.6988852779816
 -profit/volume = 0.6988852779816

 DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_LOSS),13) = 0.6990074094785
 DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT),13) = 0.6988852779816
*/
Documentation on MQL5: Trade Functions / OrderCalcProfit
Documentation on MQL5: Trade Functions / OrderCalcProfit
  • www.mql5.com
OrderCalcProfit - Trade Functions - MQL5 Reference - Reference on algorithmic/automated trading language for MetaTrader 5
Fernando Carreiro  

In actuality, it is not a bug in the OrderCalcProfit() function, and it works as expected. The problem or “bug” is in the naming of the SYMBOL_TRADE_TICK_VALUE_PROFIT and SYMBOL_TRADE_TICK_VALUE_LOSS properties. They would be better named the tick value based on “Ask” and “Bid”, instead of “Profit” and “Loss”.

If you consider them that way, then for a Buy position that closes on the Bid, one would require the tick value for bid (which is the “SYMBOL_TRADE_TICK_VALUE_LOSS”), and for a Sell position that closes on the Ask, one would require the tick value for ask (which is the “SYMBOL_TRADE_TICK_VALUE_PROFIT”).

In both cases, however, the calculations are only approximations, because both the Bid and Ask values would change by the time (and price) it reaches one of the stops.

This naming problem of these two properties has been reported and extensively covered on the Russian forum (can’t remember the link), but MetaQuotes has ignored it and not changed the names.

amrali  

The benchmark here is the tick value, itself.

So, let's calculate the tick value manually, and compare our results.

#define PRINT(A) Print(#A + " = ", (A))
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   string symbol = "AUDCAD";   // any symbol whose profit currency != account currency.
   double volume = 1e11;       // increase precision from 2 places (rounding done by OrderCalcProfit) to 13 places (11 + 2).
   double profit;
   double price = SymbolInfoDouble(symbol, SYMBOL_BID);    // try any price, even 1.11 or 3.33 will do.
   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);

//--- try buy and sell trades, each profitable and losing.
   Print("Buy orders:");
   if(OrderCalcProfit(ORDER_TYPE_BUY, symbol, volume, price, (price + ticksize), profit))  {  PRINT(profit/volume);  }
   if(OrderCalcProfit(ORDER_TYPE_BUY, symbol, volume, price, (price - ticksize), profit))  {  PRINT(-profit/volume); }

   Print("");
   Print("Sell orders:");
   if(OrderCalcProfit(ORDER_TYPE_SELL, symbol, volume, price, (price - ticksize), profit))  {  PRINT(profit/volume);  }
   if(OrderCalcProfit(ORDER_TYPE_SELL, symbol, volume, price, (price + ticksize), profit))  {  PRINT(-profit/volume); }

//--- surprise!
   Print("");
   PRINT(DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_LOSS),13));
   PRINT(DoubleToString(SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT),13));

   PRINT(DoubleToString(CalculateTickValue(symbol,false),13));
   PRINT(DoubleToString(CalculateTickValue(symbol,true),13));
  }

//+------------------------------------------------------------------+
//| Calculate tick value for a profitable or losing position.        |
//| = ProfitPerLotTick in account currency.                          |
//+------------------------------------------------------------------+
double CalculateTickValue(const string pSymbol, bool profit = true)
  {
//--- calculate tick value (profit/loss) in symbol's profit currency.
   double TickSize = SymbolInfoDouble(pSymbol,SYMBOL_TRADE_TICK_SIZE);
   double LotSize = SymbolInfoDouble(pSymbol,SYMBOL_TRADE_CONTRACT_SIZE);
   double TickValue = LotSize * TickSize;

//--- converting into deposit currency.
   const string prof = SymbolInfoString(pSymbol,SYMBOL_CURRENCY_PROFIT);
   const string acc  = AccountInfoString(ACCOUNT_CURRENCY);
   if(prof != acc)
     {
      if(profit)
         // exchange profit in profit currency -> account currency
         TickValue *= GetExchangeRate(prof, acc);

      else
         // exchange account currency -> symbol's profit currency
         // to pay and cover the loss in profit currency.
         TickValue /= GetExchangeRate(acc, prof);
     }

   return TickValue;
  }
//+------------------------------------------------------------------+
//| Returns the exchange rate for the "from/to" currency pair.       |
//+------------------------------------------------------------------+
double GetExchangeRate(string from_currency, string to_currency)
  {
   string symbol = NULL;
//---
   if(from_currency != to_currency)
     {
      //--- Try the direct rate
      symbol = GetSymbolByCurrencies(from_currency, to_currency);
      if(symbol != NULL)
        {
         double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
         return (!bid) ? -1 : bid;
        }
      //--- Try the indirect rate
      symbol = GetSymbolByCurrencies(to_currency, from_currency);
      if(symbol != NULL)
        {
         double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
         return (!ask) ? -1 : (1 / ask);
        }
      //--- Try the cross rate through US dollar
      return GetExchangeRate(from_currency, "USD") * GetExchangeRate("USD", to_currency);
     }
//---
   return (1.0);
  }
//+------------------------------------------------------------------+
//| Returns a currency pair with the specified base/profit currency. |
//+------------------------------------------------------------------+
string GetSymbolByCurrencies(string base_currency, string profit_currency)
  {
   string symbol = NULL;
//---
   bool is_custom = false;
   if(SymbolExist(base_currency + profit_currency, is_custom))
     {
      symbol = base_currency + profit_currency;
     }
//---
   return (symbol);
  }
//+------------------------------------------------------------------+

Am I missing something here, or is it really an error in the calculations done by OrderCalcProfit()?

Fernando Carreiro  
amrali #: The benchmark here is the tick value, itself. So, let's calculate the tick value manually, and compare our results. Am I missing something here, or is it really an error in the calculations done by OrderCalcProfit()

Your example code may be a little too convoluted for the demonstration. You also have not shown any output, so we have no idea what values you are getting.

Instead of AUDCAD and unknown account currency, use a simpler demonstration, for example on EURUSD with a EUR account currency.

For this simple case, the Ask tick value is simply 1.0/Ask and Bid tick value is simply 1.0/Bid.

If you then compare these to the values of SYMBOL_TRADE_TICK_VALUE_PROFIT and SYMBOL_TRADE_TICK_VALUE_LOSS, you will see that they match.

EDIT: Or maybe I misunderstood your last post and my comments are off track.

Fernando Carreiro  

If my previous post is off-track and it probably is, then see if OrderCalcProfit is perhaps taking into the account the possible value of the tick at the target price.

For example, taking EURUSD and EUR account currency,

  • at current bid price 1.06430, bid tick value is 0.939584703561026
  • at take-profit price (+100 pips) of 1.07430, bid tick value would be 0.9308386856557759
amrali  

@Fernando Carreiro, Yes, you were right about the dependence between tick_value and order_type .

Actually, MT5 calculates the tick value based on the order_type of the trade, but not based on the trade outcome (profit/loss). This seems counterintuitive, but this is the fact.

I have confirmed that this is the behavior of OrderCalcProfit():

  1. All Buy orders are calculated using SYMBOL_TRADE_TICK_VALUE_LOSS.
  2. All Sell orders are calculated using SYMBOL_TRADE_TICK_VALUE_PROFIT.
  3. Only for Forex pairs: If symbol's base currency == account currency, the above TickValue is adjusted to future rate (closing price).

#define TICKVALUE_FIXED 1

//+------------------------------------------------------------------+
//| Calculate the expected profiit for the trade operation planned.  |
//+------------------------------------------------------------------+
double OrderCalcProfit2(ENUM_ORDER_TYPE ordertype, string symbol, double volume, double price_open, double price_close)
  {
   double profit=0.0;
   double TickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);

   // Calculation of the tick_value depends on the order type.
   double TickValue= 0.0;
   if(ordertype == ORDER_TYPE_BUY)  TickValue = SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_LOSS);
   if(ordertype == ORDER_TYPE_SELL) TickValue = SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT);

// fix tick_value for non-forex symbols.
#ifdef TICKVALUE_FIXED
   TickValue = mTickValue(symbol,(bool)ordertype);  // https://www.mql5.com/en/code/viewcode/28029/267359/functions.mqh
#endif

//--- If account currency == base currency, adjust TickValue to future rate (close). Works only for Forex pairs.
   const string AccountCurrency = AccountInfoString(ACCOUNT_CURRENCY);
   const string BaseCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
   const string ProfitCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
   if((AccountCurrency == BaseCurrency) && (AccountCurrency != ProfitCurrency))
     {
      if(ordertype == ORDER_TYPE_BUY)  TickValue *= SymbolInfoDouble(symbol, SYMBOL_BID) / price_close;
      if(ordertype == ORDER_TYPE_SELL) TickValue *= SymbolInfoDouble(symbol, SYMBOL_ASK) / price_close;
     }

//--- PL = lots * ticks delta * tickvalue
   if(ordertype == ORDER_TYPE_BUY)  profit = volume * (price_close - price_open) / TickSize * TickValue;
   if(ordertype == ORDER_TYPE_SELL) profit = volume * (price_open - price_close) / TickSize * TickValue;

   profit = NormalizeDouble(profit, (int)AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS));

   return(profit);
  }

The above function yields the exact results as the builtin OrderCalcProfit().

Edit:

It would be better if the names were SYMBOL_TRADE_TICK_VALUE_BUY (for SYMBOL_TRADE_TICK_VALUE_LOSS) and SYMBOL_TRADE_TICK_VALUE_SELL (for SYMBOL_TRADE_TICK_VALUE_PROFIT), to avoid this confusion. Docs say nothing about this.

Fernando Carreiro  

amrali #: @Fernando Carreiro, Yes, you were right about the dependence between tick_value and order_type. Actually, MT5 calculates the tick value based on the order_type of the trade, but not based on the trade outcome (profit/loss). This seems counterintuitive, but this is the fact.

I have confirmed that this is the behavior of OrderCalcProfit():

  1. All Buy orders are calculated using SYMBOL_TRADE_TICK_VALUE_LOSS.
  2. All Sell orders are calculated using SYMBOL_TRADE_TICK_VALUE_PROFIT.
  3. Only for Forex pairs: If symbol's base currency == account currency, the above TickValue is adjusted to future rate (closing price).

The above function yields the exact results as the builtin OrderCalcProfit().

Yes, and it is not counter intuitive because as I mentioned in my post #1

  • Buy positions close at the Bid price irrespective of Profit or Loss, and the SYMBOL_TRADE_TICK_VALUE_LOSS should actually be called SYMBOL_TRADE_TICK_VALUE_BID.
  • Sell positions close at the Ask price irrespective of Profit or Loss, and the SYMBOL_TRADE_TICK_VALUE_PROFIT should actually be called SYMBOL_TRADE_TICK_VALUE_ASK.
The only problem is that the names for the enumerations are incorrect.
Alain Verleyen  
Fernando Carreiro #:

Yes, and it is not counter intuitive because as I mentioned in my post #1

  • Buy positions close at the Bid price irrespective of Profit or Loss, and the SYMBOL_TRADE_TICK_VALUE_LOSS should actually be called SYMBOL_TRADE_TICK_VALUE_BID.
  • Sell positions close at the Ask price irrespective of Profit or Loss, and the SYMBOL_TRADE_TICK_VALUE_PROFIT should actually be called SYMBOL_TRADE_TICK_VALUE_ASK.
The only problem is that the names for the enumerations are incorrect.

They are not, and it's anyway irrelevant for this topic on OrderCalcProfit().

Forum on trading, automated trading systems and testing trading strategies

SYMBOL_TRADE_TICK_VALUE_LOSS vs SYMBOL_TRADE_TICK_VALUE_PROFIT

Alain Verleyen, 2023.02.18 21:40

You are both wrong. The tick value is independent of the direction of a trade (BUY or SELL).

The market is closed, so I can't prove it with data for now.

But let see some theory in the first place :

What is a tick ? It's the smallest price movement for a considered symbol. Let's take EURUSD, with a 5 digits standard quotation, so a tick is 0.00001 (tick size).

What is a tick value ? is the value, in account currency, of this minimal price movement (tick or tick size), considering a traded volume of 1 lot (let's say a standard lot so 100,000 units).

How do we calculate the value of a tick ?

  • Firstly we get the value considering the traded symbol, the unit is always the second currency in a pair (USD in our case), by the quotation definition. So the profit/loss, for 1 standard lot is 0.00001 * 100000 = 1 USD. Or 100 JPY (0,001 * 100000) on EURJPY, or 1 CAD on USDCAD (0.00001 * 100000), etc...
  • Secondly, if necessary, make the conversion to account currency and it's in this step that relies all the subtleties. As yes, you have to take into account if it's a loss or a profit, and additionally if the conversion is "direct" (from USD to a JPY account using USDJPY) or "indirect" (from JPY to USD account using JPYUSD). The conversion rate will of course use either the bid or the ask, of the symbol needed for the conversion which may be the same as the traded symbol OR NOT, so this is completely independent of the considered trade direction !

Let's take some examples.


Account in EUR. All trades will require a conversion because no symbol is using EUR as profit (loss) currency.

  • EURUSD trade. BUY or SELL, doesn't matter.

In profit, we have win 10 USD, we need to sell them to get EUR, as the symbol is EURUSD we will BUY EURUSD at ask price.

In loss, we have lost -10 USD, we need to sell some EUR to pay for it, so we will SELL EURUSD at bid price.

  • GBPJPY trade. BUY or SELL, doesn't matter.

In profit, we win 2500 JPY, we need to sell them by buying EUR, so we BUY EURJPY at ask price.

In loss, we have lost -2500 JPY, we need to sell some EUR to pay for it, we will SELL EURJPY at bid price.

So with an EUR account, SYMBOL_TRADE_TICK_VALUE_PROFIT is always the ASK price of the conversion symbol needed, which may or may not be the traded symbol. And SYMBOL_TRADE_TICK_VALUE_LOSS is always the bid price of the conversion symbol.


Account USD. Here, we have more possibilities.

  • EURUSD trade. BUY or SELL, doesn't matter.

In profit, or in loss, there is no need for conversion, as the profit currency is USD and the account currency is USD too. By the way it's the profit that the trade direction doesn't matter, but the conversion matters.

  • GBPJPY trade. BUY or SELL, doesn't matter.

In profit, we win 2500 JPY, we need to sell them by buying USD, so we BUY USDJPY at ask price.

In loss, we have lost -2500 JPY, we need to sell some USD to pay for it, we will SELL USDJPY at bid price.

  • EURNZD trade. BUY or SELL, doesn't matter.

In profit, we win 15 NZD, we need to sell them by buying USD, as the symbol is NZDUSD we will SELL NZDUSD at bid price.

In loss, we have lost -15 NZD, we need to buy some NZD to pay it, we will BUY NZDUSD at ask price.

Symbols with USD as the profit (loss) currency will not need conversion. Symbols with direct conversion will have SYMBOL_TRADE_TICK_VALUE_PROFIT matching bid conversion price, and SYMBOL_TRADE_TICK_VALUE_LOSS will match the ask conversion price. In reverse, for symbols with "indirect" conversion, SYMBOL_TRADE_TICK_VALUE_PROFIT matching ask conversion price, and SYMBOL_TRADE_TICK_VALUE_LOSS will match the bid conversion price.


Account JPY.

I will not give the details, the logic is the same.

There is never a conversion needed when JPY is implied, as JPY is always the profit/loss currency.

When a conversion is needed the conversion is always "direct" as JPY is always the second part of a symbol, so on an JPY account, SYMBOL_TRADE_TICK_VALUE_PROFIT is always the BID price of the conversion symbol needed, which is never the traded symbol. And SYMBOL_TRADE_TICK_VALUE_LOSS is always the ask price of the conversion symbol.


There are some specific cases, mainly when the trading is in a more "exotic" currency, like for example SGD (Singapour dollar) where it happens the crosses pairs needed for conversion is not available. Say a CADCHF trade, which would require a SGDCHF symbol to convert in CHF profit/loss in SGD, but this SGDCHF pair is not available, in such case, there will be a double conversion using USD. So from CHF to USD using USDCHF, then from USD to SGD using USDSGD.


Alain Verleyen  
amrali #:

@Fernando Carreiro, Yes, you were right about the dependence between tick_value and order_type .

No, he is not right.

Actually, MT5 calculates the tick value based on the order_type of the trade, but not based on the trade outcome (profit/loss). This seems counterintuitive, but this is the fact.

I have confirmed that this is the behavior of OrderCalcProfit():

  1. All Buy orders are calculated using SYMBOL_TRADE_TICK_VALUE_LOSS.
  2. All Sell orders are calculated using SYMBOL_TRADE_TICK_VALUE_PROFIT.
  3. Only for Forex pairs: If symbol's base currency == account currency, the above TickValue is adjusted to future rate (closing price).

The above function yields the exact results as the builtin OrderCalcProfit().

Edit:

It would be better if the names were SYMBOL_TRADE_TICK_VALUE_BUY (for SYMBOL_TRADE_TICK_VALUE_LOSS) and SYMBOL_TRADE_TICK_VALUE_SELL (for SYMBOL_TRADE_TICK_VALUE_PROFIT), to avoid this confusion. Docs say nothing about this.

The OrderCalcProfit(), and SYMBOL_TRADE_TICK_VALUE_XXX doesn't matter. As stated by Renat Fathkullin, Metaquotes CEO there are not used at all by MT5 to calculated actual profit or loss.

AND anyway they are only indicative because what matters is the price at the CLOSE of a trade, at that time using these functions is irrelevant. So yes it seems you discovered a bug in OrderCalcProfit(), but who cares ? why are you thinking it's a serious bug ?

amrali  

To make it short:

Let's take for example how tick values are calculated on USD accounts:

Tick_value Buy USDJPY = CONTRACT_SIZE * TICK_SIZE * 1/SYMBOL_BID (of USDJPY)   (= rate of account/profit currency)

Tick value Buy EURGBP = CONTRACT_SIZE * TICK_SIZE * SYMBOL_ASK (of GBPUSD)     (= rate of profit/account currency)

If you are not using the provided tick_value, we can calculate it using the conversion rates (symbol's profit currency -> account currency). Selecting of BID or ASK price for those conversion rates depends on the type of your trade (BUY vs SELL) and the position of account currency within the conversion pair (first or second), not the trade outcome as I said before.

I said that Fernando is right about his approximation for the names only (as all Buy orders close on BID and their profits are calculated using TICK_VALUE_LOSS).

The price type (BID or ASK) is irrelevant for the calculation of profits and tick values.. (ONLY the order_type decides which tick_value_xxx to use).

//+------------------------------------------------------------------+
//| Calculate the expected profiit for the trade operation planned.  |
//+------------------------------------------------------------------+
double OrderCalcProfit3(ENUM_ORDER_TYPE ordertype, string symbol, double volume, double price_open, double price_close)
  {
//--- PL = number of units * price delta * profit/account exchange rate
   double profit=0.0;
   double ContractSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   if(ordertype == ORDER_TYPE_BUY)  profit = volume * ContractSize * (price_close - price_open);
   if(ordertype == ORDER_TYPE_SELL) profit = volume * ContractSize * (price_open - price_close);

//--- converting into account currency.
   const string AccountCurrency = AccountInfoString(ACCOUNT_CURRENCY);
   const string BaseCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
   const string ProfitCurrency = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
   if(AccountCurrency != ProfitCurrency)
     {
      //--- If account currency == base currency, adjust profits to future rate (close). Works only for Forex pairs.
      if(AccountCurrency == BaseCurrency)
        {
         profit *= 1.0 / price_close;
        }
      //--- find the profit/account exchange rate.
      else
         profit *= GetExchangeRate(ProfitCurrency, AccountCurrency, ordertype);  // https://www.mql5.com/en/code/viewcode/28029/267359/functions.mqh
     }

   profit = NormalizeDouble(profit, (int)AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS));

   return(profit);
  }
  
Reason: