Particularidades del trabajo con números del tipo double en MQL4

MetaQuotes | 13 febrero, 2012

Introducción

Al programar en el lenguaje MQL4, los principiantes a veces se encuentran con situaciones en las que los resultados de ciertos cálculos matemáticos se diferencian de los esperados. Así y con todo, el programa se compila y funciona, pero no de la forma necesaria. Entonces los principiantes empiezan a verificar el programa, encuentran nuevos "errores" en el lenguaje, en la implementación de las funciones, etc. En la mayoría de los casos, el análisis posterior muestra que todo va bien en el lenguaje y el compilador, mientras que el texto del código ocultaba un error enojoso cuya búsqueda puede llevar mucho tiempo.

En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.


1 Control de los valores numéricos

Para comprobar los resultados de los cálculos y la depuración de programas, se puede usar la función string DoubleToStrMorePrecision(double number, int precision); de la biblioteca estándar stdlib.mq4, que permite controlar los valores numéricos de los números del tipo double hasta el dígito indicado.

Esto permitirá ahorrar tiempo al buscar posibles errores.

Ejemplo de uso:
#include <stdlib.mqh>
int start()
  {
   double a=2.0/3;
   Alert("Standard output:",a,", 8 digits precision:",DoubleToStr(a,8),", 15 digits precision:", DoubleToStrMorePrecision(a,15));
   return(0);
  }  

Resultado:

Standard output:0.6667, 8 digits precision:0.66666667, 15 digits precision:0.666666666666667


En muchos casos, al introducir los valores numéricos de los números con punto flotante (por ejemplo, al usar Print, Alert, Comment) en lugar de recurrir a la muestra estándar (solo los 4 primeros números tras la coma), es mejor utilizar la función DoubleToStrMorePrecision para controlar con mayor precisión los valores numéricos.

Ejemplo del código:

#include <stdlib.mqh>
int start()
  {
   double a=2.0/100000;
   Alert("Standard output=",a,", More precise output=",DoubleToStrMorePrecision(a,15));
   return(0);
  }

como resultado, muestra: "Standard output=0, More precise output=0.000020000000000".


2. La precisión al trabajar con números del tipo double

Las especificidades del formato de guardado de números double en la computadora provocan la limitación de la precisión de su almacenamiento y la aparición de imprecisiones al trabajar con ellos.

Por ejemplo, al usar la precisión ilimitada de cálculo, para cualquier número A y B siempre serán válidas las expresiones:

(A/B)*(B)=A,

A-(A/B)*B=0,

(A/B)*(B/A)=1, etcétera.

En la computadora, la precisión de guardado de la cantidad de dígitos decimales de los números del tipo doube viene definida por el tamaño de la mantisa y está limitada a 52 bits.

Veamos el siguiente ejemplo, que ilustra la pérdida de precisión indicada. El programa mostrado más abajo calcula en el ciclo (i) el producto de los números enteros hasta 23 (23!=25852016738884976640000), el resultado de los cálculos se guarda en una variable (a) del tipo double. En el siguiente ciclo (j) se divide el número (a) por cada uno de los números enteros hasta 23. Como resultado, lo lógico sería esperar a=1.

#include <stdlib.mqh>
int start()
  {
   int maxfact=23;
   double a=1;
   for (int i=2; i<=maxfact; i++) { a=a*i; }
   for (int j=maxfact; j>=2; j--) { a=a/j; }
   Alert(" a=",DoubleToStrMorePrecision(a,16));
   return(0);
  }

Y sin embargo, tenemos:

a=1.0000000000000002

De esta forma, al trabajar con números enteros, tenemos una imprecisión en el 16 dígito.

Si aumentamos el cálculo a 35!, obtendremos a=0.9999999999999998.

En el lenguaje MQL existe la función NormalizeDouble, que permite redondear un número del tipo double hasta la precisión indicada.

Puesto que las constantes del tipo doble se guardan en la memoria de una forma parecida a los números del tipo double, al definir una constante es necesario tener en cuenta la limitación de 15 dígitos de las cifras tras la coma.

Sin embargo, no es conveniente confundir la precisión de representación de los números double, analizada más arriba, con los límites de sus cambios, que son bastante más amplios: de -1.7*e-308 a 1.7*e308.

Podemos valorar de forma aproximada el exponente mínimo del número double, que será indistinguible de 0, con la ayuda del código siguiente:

int start()
  {
  double R=1;
  int minpwr=0;
  while (R>0) {R=R/10; minpwr--;}
  Alert(minpwr);
  return(0);
  }



3. La función NormalizeDouble

La función double NormalizeDouble (double value, int digits) redondea el número value con una precisión de digits dígitos.

En el ejemplo:

int start()
  {
   double a=3.141592663589;
   Alert("a=",DoubleToStr(NormalizeDouble(a,5),8));
   return(0);
  }

el resultado será

a=3.14159000

Es imprescindible usar NormalizeDouble() al utilizar números del tipo double como argumentos de las funciones para ejecutar operaciones comerciales. En las operaciones comerciales no se deben utilizar precios no normalizados cuya precisión supere aunque sea en un dígito la requerida por el servidor comercial.

Los valores calculados StopLoss, TakeProfit, así como los valores del precio de apertura de las órdenes pendientes deberán ser normalizados con una precisión cuyo valor se guardará en una variable Digits predeterminada.


4. Peculiaridades en la comparación de números del tipo double

Es recomendable realizar la operación de comparación de la igualdad de dos números double con la ayuda de la función bool CompareDoubles(double number1,double number2) de la biblioteca estándar stdlib.mq4, que tiene el aspecto:

//+------------------------------------------------------------------+
//| correct comparison of 2 doubles                                  |
//+------------------------------------------------------------------+
bool CompareDoubles(double number1,double number2)
  {
   if(NormalizeDouble(number1-number2,8)==0) return(true);
   else return(false);
  }

Esta función compara los números number1 y number2 del tipo double con una precisión de 8 dígitos decimales.

Ejemplo:

#include <stdlib.mqh>
int start()
  {double a=0.123456781;
   double b=0.123456782; 
   if (CompareDoubles(a,b)) {Alert("They are equal");}
   else {Alert("They are different");}
  }


mostrará

They are equal

puesto que los números a y b se diferencian solo en el 9 dígito.

En caso necesario, el uso de una precisión más elevada se puede escribir de forma análoga en su propia función de comparación hasta la precisión necesaria.


5. División de números enteros

Debemos recordar que al dividir dos números enteros, el resultado será un número entero.

Por eso el código:

int start()
  {
   Alert(70/100);
   return(0);
  }


mostrará 0, puesto que 70 y 100 son valores enteros. Al igual que en el lenguaje C, en MQL4 el resultado de la división de un número entero por otro no será un número entero, en este caso, será 0.

Sin embargo, si uno de sus valores es un número del tipo double (es decir, tiene una parte fraccionada), el resultado de la división será un número del tipo double. Por eso, Alert(70/100.0); dará el número 0.7. Asimismo, debemos recordar las normas de conversión de tipos y formalizar con más cuidado la expresión.

Ejemplo del código:

int start()
  { double a=1/3;
    double b=1.0/3;
   Alert("a=",a,", b=",b);
   return(0);
  }


dará a=0, b=0.3333


6. Conversión de tipos para integer y double

Vamos a ver este segmento de código:
double xbaseBid=1.2972;
double xBid=1.2973;
double xPoint=0.0001;
int i = 100 + (xBid - xbaseBid)/xPoint;
Alert(i);


Como resultado del trabajo, se mostrará el número 100, aunque parece que (i) debería ser igual a 101, dado que 0.0001/0.0001=1.

Un ejemplo análogo en C/C++:

double baseBid=1.2972,Bid=1.2973,Point=0.0001;
int i = 100 + (Bid - baseBid)/Point;
printf("%d\n",i);

también dara 100.

Para investigar los motivos de esta circunstancia, analizaremos el código:
double a=0.99999999999999;
int i = 100 + a;
Alert(i);

El resultado del funcionamiento será la muestra del número i=100.

Sin embargo, si mejoramos la precisión de la cifra a:

double a=0.999999999999999;
int i = 100 + a;
Alert(i);

obtendremos 101. La causa es el uso mezclado de números enteros y números con punto flotante, el motivo empeora por la reducida precisión de los valores.

Por eso, al realizar operaciones de este tipo, se recomienda redondear estas expresiones con la ayuda de la función double MathRound(double value), que retorna un valor redondeado hasta el número entero más próximo al valor numérico indicado:

double baseBid=1.2972;
double xBid=1.2973;
double xPoint=0.0001;
int i = 100 + MathRound((xBid - baseBid)/xPoint);
Alert(i);

En este caso, obtendremos el valor correcto 101.

Con frecuencia nos encontramos con un error (sobre todo en los fragmentos de código responsables del Trailing Stop), cuando no se realiza correctamente la comparación de números del tipo double y se usan posteriormente al llamar la función OrderModify(), que cuando se intentan cambiar los mismos parámetros ya establecidos, da el error número 1: ERR_NO_RESULT.

Por eso debemos comprobar minuciosamente las operaciones de comparación (recuerde la normalización) y las expresiones semejantes para calcular la cantidad de puntos. Es conveniente recordar que el terminal permite cambiar las órdenes con la función OrderModify solo en el caso de que los nuevos valores numéricos se diferencian de los antiguos aunque sea en 1 punto.


7. Peculiaridades de la función MathMod

El resultado del funcionamiento de la función MathMod(double v1, double v2) en el lenguaje MQL4 se corresponde totalmente con el resultado del funcionamiento de la función fmod(double v1, double v2) de la biblioteca matemática MSVC6, puesto que al ser ejecutada se usa la llamada directa de esta función en C Runtime Library. En algunos casos, la función fmod en MSVC6 (y por consiguiente MathMod), da un resultado incorrecto.

Si en sus programas se usa esta función, sustituya la llamada de MathMod por la llamada de la siguiente función, que siempre retorna el resultado correcto:

double MathModCorrect(double a, double b)
{ int tmpres=a/b;
return(a-tmpres*b);
}


Es necesario destacar que esta peculiaridad solo tiene lugar en MQL4, en MQL5 el cálculo de esta función se realiza de acuerdo con la definición matemática de la función.


Conclusión

Es necesario aclarar que la lista mostrada aquí no es exhaustiva, si usted ha detectado alguna nueva situación no descrita en estos apuntes, mire los comentarios más abajo. Si aún no ha encontrado una solución, describa la situación usted mismo en los comentarios, adjunte el fragmento de código correspondiente y los especialistas de la comunidad MQL4 intentarán ayudarle.