Bid/Ask: (No Need) to use NormalizeDouble in OrderSend

 

EDIT: Whilst it seems to me that this Normalization used to be a problem, the experimental test indicators and scripts below have convinced me that this is not (or is no longer) a problem (at least with the latest MT4 and with my broker). The thread remains primarily for the indicator/EA below, which allows testing with your setup, and for the helpful comments from Raptor and WHR.


You can glance through the MQL4 Code base and see how many EAs do not use NormalizeDouble on the Bid and Ask prices in the OrderSend function. It is a large number.

Arguably the problem here is that most of the time it works! However a 10% failure rate is pretty poor reliability if you ask me.

I have written this little indicator to demonstrate the problem.

#property indicator_chart_window

int errors=0;
int tickCount=0;
string errStr="";

int start(){
   
   tickCount++;
   
   // The constant 1E12 has been written out in thousands to make it easier to read.
   // The compiler will (hopefully) sort it out efficiently. Constants of the form 1Exx are not accepted.
   double delta = MathAbs( (1000.0 * 1000.0 * 1000.0 * 1000.0 * (NormalizeDouble(Bid,Digits) - Bid))/Point );
   
   if( delta > 0.0 ){
      errors++;
      errStr = "Last error was " + DoubleToStr( delta, 2 );
   }
   
   Comment( "Un-normalized rate is " + errors + " per " + tickCount + " ticks\n" + errStr );
  
   return(0);
}

I have been running this script on 6 pairs this morning for just a few minutes. Since today is a Bank Holiday the tick rate is quite slow, but nevertheless 4 of the 6 pairs tested showed an error rate of between 5% and 20%. Two pairs showed no errors at all during the few minutes of the test.

I find it interesting that the error displayed was 22.20 on all 4 pairs. I was not expecting the rounding error to always be the same.

The point is you can get away with it on certain pairs, when at certain prices, but in general you will be making an unreliable EA if you do not Normalize the prices before using them in the OrderSend function. This seems entirely unreasonable but nevertheless if you don’t do it then bitching and whining about how somebody else’s code is to blame will not stop failed trades.

 

I think you need to read this thread that I started . . . https://www.mql5.com/en/forum/136997 I think the issue you are seeing is simply down to the way Floating point numbers are implemented.

 
RaptorUK:

I think you need to read this thread that I started . . . https://www.mql5.com/en/forum/136997 I think the issue you are seeing is simply down to the way Floating point numbers are implemented.


Well there is some interesting info in that thread. The real question is: "Is the Bid price adequately normalized on its own?" I think you are suggesting that it is, but it is normalized slightly differently to when the NormalizeDouble() function is used. So we have at least two approximations to an exact value.

I think we can agree that you absolutely have to re-normalize when doing something like

(Ask - stopLoss * Point)

but just using Bid, Ask or OrderClosePrice()

on their own is perhaps less obvious.

EDIT:

Of course an opinion is much more valuable if it is testable and falsifiable. So I have written a little EA to test whether or not an un-normalized Bid/Ask/OrderClosePrice() are ok to use as is. Certainly, by test, using the NormalizeDouble() function on these items can return a different answer.

My EA below waits for the OrderClosePrice() to be different to a Normalized version before closing the position.

#include <stdlib.mqh>

int tickCount = 0;

int start(){
   
   tickCount++;
   
   int openOrders = OrdersTotal();
   if( openOrders > 0 ){
     
      for( int n=0; n<openOrders; n++ ){
         if( !OrderSelect(n,SELECT_BY_POS) )
            continue;
      
         if( OrderSymbol() != Symbol() )
            continue;
            
         // The constant 1E12 has been written out in thousands to make it easier to read.
         // The compiler will (hopefully) sort it out efficiently. Constants of the form 1Exx are not accepted.
         double close = OrderClosePrice();
         double delta = MathAbs( (1000.0 * 1000.0 * 1000.0 * 1000.0 * (NormalizeDouble(close,Digits) - close))/Point );
   
         if( delta > 0.0 ){   // now is the worst possible time to close the trade :-)
            if( !OrderClose(OrderTicket(), OrderLots(), close, 30) ){
               Print( ErrorDescription( GetLastError() ));
            }
            return( 0 );
         }
      }
   }
   
   Comment("Waiting for an un-normalized CLOSE price\n" + tickCount + " ticks elapsed");

   return(0);
}

I tried this on a demo account, closing 5 positions one at a time. There was no problem. Conclusion: Even though Bid/Ask/OrderClosePrice() are different if normalized, they are still adequate for trading purposes (and therefore work as you would expect/hope them too.)

 
dabbler:

I think we can agree that you absolutely have to re-normalize when doing something like

(Ask - stopLoss * Point)

Nope, I don't think I agree . . . if Ask is OK on it's own then adding or subtracting a number of points should be just fine . . . I think, I know what WHR say on the subject . . . I have a lot of respect for his views.
 

Yep, you do, but I'll say it again.

It is never necessary to normalizeDouble, ever. It's a cludge, don't use it.

  1. Open price for pending order must be adjusted to be a multiple of ticksize, not point, and on metals they are not the same.
    double NormalizePrice(double p){
        double ts = MarketInfo(Symbol(), MODE_TICKSIZE);
        return( MathRound(p/ts)*ts );
    }
  2. Lot size must be adjusted to be a multiple of lotstep, which may not be a power of ten on some brokers
    double NormalizeLots(double p){
        double ls = MarketInfo(Symbol(), MODE_LOTSTEP);
        return( MathRound(p/ls)*ls );
    }
  3. NormalizeDouble(x) != x will occur even for Bid/Ask
  4. If you need to compare to equality (or greater or equal) and the equal is important you must take extra steps
    if (price >= trigger) ...
    may or may not work at trigger. I had this
    string  PriceToStr(double p){ return( DoubleToStr(p, Digits) ); }
    :
    Print("Bid(",PriceToStr(Bid),"<cab(",PriceToStr(ca.below),")=",Bid<ca.below);
    // Printed: Bid(1.25741)<cab(1.25741)=1
    
    Even printed to 5 digits the two values looked equal but the Bid<ca returned true.
    If the trigger on the exact value is important then use this form (note no equals)
    double point_2 = Point / 2;
    if (price + point_2 > trigger) ...

 
RaptorUK:
Nope, I don't think I agree . . . if Ask is OK on it's own then adding or subtracting a number of points should be just fine.
Well it turns out that I don't even agree myself :-)

In principle, taking the rounding error on Point and multiplying it by 1000 or so gives a much larger rounding error. However it turns out that even that larger error is not (or no longer) a problem.

--------------------------------------------------------------------------------------------------------------

Well, well, what do you know. My latest test EA deliberately adds an error to the Ask price to make the OrderSend() fail. It only fails when the Ask price is wrong by a whole point! I am convinced that it never used to do this, but I have no way to prove it.

I can imagine that either the broker has changed their receiving software so it is not fussy to the 12th digit of price or that MetaQuotes has tweaked the Order Send() function so it doesn’t send these extra meaningless precision values.

(Don’t use this on a Live Account unless you are willing to throw away money).

#include <stdlib.mqh>

int count = -10;
double stopLoss =   1000.0;    // measured in points
double takeProfit = 1000.0;    // measured in points

int init(){
   count = -10;
   return( 0 );
}

int start(){
   
   int openOrders = OrdersTotal();
   if( openOrders > 0 ){
     
      for( int n=0; n<openOrders; n++ ){
         if( !OrderSelect(n,SELECT_BY_POS) )
            continue;
      
         if( OrderSymbol() != Symbol() )
            continue;
         
         // just close the open order to get rid of it   
         if( !OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 30) )
            Print( ErrorDescription( GetLastError() ));

         return( 0 );
      }
   }
   
   if( count > 0 )
      return( 0 );   

   double error = MathPow(10.0, count );  
   Comment( count + "\nError of " + DoubleToStr(error/Point, 8) + " points." );

   // Open a LONG position
   double SL = Ask - stopLoss   * Point + error;
   double TP = Ask + takeProfit * Point + error;
   
   // add increasingly large errors to the Ask price to see at what point it falls overs
   if( OrderSend( Symbol(), OP_BUY, 0.01, Ask + error, 100, SL, TP )== -1 ){
      Print(   "FAILED when error term was 10^" + count + " which is " + DoubleToStr(error/Point, 8) + " points." );
      Comment( "FAILED when Error term was 10^" + count + " which is " + DoubleToStr(error/Point, 8) + " points." ); 
      Print( ErrorDescription( GetLastError() ));
      count += 100;  // lock out further activity
   }
      
   count++;   
   Sleep(5000);
   
   return(0);
}

In any case the OrderSend() is (now) very robust and working exactly as you would hope/want/expect it to :-)

 
WHRoeder:

Yep, you do.

It is never necessary to normalizeDouble, ever. It's a cludge, don't use it.

  1. Open price for pending order must be adjusted to be a multiple of ticksize, not point, and on metals they are not the same.
  2. Lot size must be adjusted to be a multiple of lotstep, which may not be a power of ten on some brokers
  3. NormalizeDouble(x) != x will occur even for Bid/Ask
  4. If you need to compare to equality (or greater or equal) and the equal is important you must take extra steps may or may not work at trigger. I had this Even printed to 5 digits the two values looked equal but the Bid<ca returned true.
    If the trigger on the exact value is important then use this form (note no equals)

Thank you for your comprehensive response.
 

WHRoeder:


Open price for pending order must be adjusted to be a multiple of ticksize, not point, and on metals they are not the same.

........

and what for metals? especially the silver?

 
Print out your broker's values
 

WHRoeder:
Print out your broker's values
......... . ..............

do you mean the tick value?

if you meant this, it's 0.000 for silver!

 
ahm_zoz:

do you mean the tick value?

if you meant this, it's 0.000 for silver!

Please don't put your reply in the quote box . . .


Print your Brokers values for MarketInfo() MODE_TICKSIZE and MODE_DIGITS

Reason: