Особенности работы с числами типа double в MQL4

MetaQuotes | 2 ноября, 2009

Введение

При программировании на языке MQL4 у новичков порой возникают ситуации, когда результаты некоторых математических вычислений отличаются от ожидаемых. При этом программа компилируется и работает, но не так, как нужно. Они начинают разбираться в программе, находят новые "ошибки" в языке, реализации функций и т.п. В большинстве случаев последующий анализ показывает, что с языком и компилятором все нормально, а вот в тексте кода закралась досадная ошибка, поиск которой может занять много времени.

В данной заметке собраны советы по решению наиболее часто возникающих ошибок при работе с числами типа double в программах на MQL4.


1. Контроль над численными значениями

Для проверки результатов расчетов и отладки программ можно использовать функцию string DoubleToStrMorePrecision(double number, int precision); стандартной библиотеки stdlib.mq4, которая позволяет проконтролировать численные значения чисел типа double до указанного знака.

Это позволит сэкономить время при поиске возможных ошибок.

Пример использования:
#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);
  }  

Результат:

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


Во многих случаях при выводе численных значений чисел c плавающей точкой (например, при использовании Print, Alert, Comment) вместо стандартного вывода (только первых 4 знаков после запятой) лучше использовать функцию DoubleToStrMorePrecision для более точного контроля численных значений.

Например код:

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

в результате выведет: "Standard output=0, More precise output=0.000020000000000".


2. Погрешности при работе с числами типа double

Специфика формата хранения чисел double в компьютере приводит к ограничению точности их хранения и возникновению погрешностей при работе с ними.

Например, при использовании бесконечной точности вычислений, для любых действительных чисел A и B всегда будут справедливы тождества:

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

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

(A/B)*(B/A)=1 и т.п.

В компьютере точность хранения количества десятичных знаков чисел типа double определяется размерами мантиссы и ограничена 52 битами.

Рассмотрим следующий пример, иллюстрирующий указанную потерю точности. Приведенная ниже программа вычисляет в цикле по i произведение целых чисел до 23 (23!=25852016738884976640000), результат расчетов хранится в переменной a типа double. Затем в цикле по j производится деление числа a на каждое из целых чисел до 23. В результате логично было бы ожидать 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);
  }

Однако в результате имеем:

a=1.0000000000000002

Таким образом при работе с целыми числами мы получили погрешность в 16-м знаке.

Если увеличить расчет до 35!, то получим a=0.9999999999999998.

В языке MQL существует функция NormalizeDouble, позволяющая округлить число типа double до указанной точности.

Поскольку константы типа double в памяти хранятся в том же виде, что и числа типа double, поэтому при определении констант необходимо учитывать ограничение на 15 значимых цифр после запятой.

Однако не следует путать рассмотренную выше точность представления чисел double с пределами их изменения - они гораздо шире: от -1.7*e-308 до 1.7*e308.

Приближенно можно оценить минимальную степень числа double, которое будет неотличимо от 0 при помощи следующего кода:

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



3. Функция NormalizeDouble

Функция double NormalizeDouble (double value, int digits), осуществляет округление числа value до точности в digits знаков.

В примере:

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

результат будет

a=3.14159000

Функцию NormalizeDouble() необходимо применять при использовании чисел типа double в качестве аргументов функций для проведения торговых операций. В торговых операциях нельзя использовать ненормализованные цены, чья точность превышает требуемую торговым сервером хотя бы на один знак.

Рассчитываемые значения StopLoss, TakeProfit, а также значения цены открытия отложенных ордеров должны быть нормализованы с точностью, значение которой хранится в предопределенной переменной Digits.


4. Особенности сравнения чисел типа double

Операцию сравнения двух чисел double на равенство рекомендуется производить при помощи функции bool CompareDoubles(double number1,double number2) стандартной библиотеки stdlib.mq4, которая имеет вид:

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

Данная функция производит сравнение чисел number1 и number2 типа double с точностью до 8 знака после запятой.

Пример:

#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");}
  }

выведет

They are equal

поскольку числа a и b различаются лишь в 9-м знаке.

В случае необходимости использования более высокой точности аналогичным образом можно написать свою функцию сравнения чисел до нужной точности.


5. Деление целых чисел

Следует помнить, что при делении двух целых чисел результатом будет целое число.

Поэтому код:

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

выведет 0, т.к. 70 и 100 - целочисленные значения. Как и в языке C, в MQL4 результатом деления целого числа на целое будет целое число, в данном случае 0.

Однако если одно из значений является числом типа double (т.е. имеет дробную часть), то результат деления будет числом типа double. Поэтому Alert(70/100.0); даст число 0.7. Также следует помнить про правила приведения типов и аккуратнее оформлять выражения.

Например код:

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

выведет a=0, b=0.3333


6. Приведение типов - integer и double

Рассмотрим участок кода:
double xbaseBid=1.2972;
double xBid=1.2973;
double xPoint=0.0001;
int i = 100 + (xBid - xbaseBid)/xPoint;
Alert(i);

В результате работы будет выведено число 100, хотя казалось бы, i должно быть равным 101, поскольку 0.0001/0.0001=1.

Аналогичный пример на C/C++:

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

тоже выдает 100.

Для исследования причины данного обстоятельства рассмотрим код:
double a=0.99999999999999;
int i = 100 + a;
Alert(i);

Результатом работы также будет вывод числа i=100.

Однако если несколько улучшить точность числа a:

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

то получим 101. Причина состоит в смешанном использовании целочисленных чисел и чисел с плавающей точкой, усугубленная пониженной точностью значений.

Поэтому при проведении операций такого типа рекомендуется проведение округления подобных выражений при помощи функции double MathRound(double value) которая возвращает значение, округленное до ближайшего целого числа, указанного числового значения:

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

В этом случае мы получим правильное значение 101.

Часто встречается ошибка (особенно в участках кода, отвечающего за Trailing Stop) при неправильном проведении сравнения чисел типа double и последующем их использовании при вызове функции OrderModify(), которая, при попытке изменения уже установленных таких же значений, выдает ошибку с номером 1: ERR_NO_RESULT.

Особенно внимательным следует быть при проведении операций сравнения (помните про нормализацию) и подобных выражений для подсчета количества пунктов. Следует помнить, что терминал допускает изменение ордеров функцией OrderModify только в том случае, если новые численные значения отличаются от старых хотя бы на 1 пункт.


7. Особенность функции MathMod

Результат работы функции MathMod(double v1, double v2) в языке MQL4 полностью соответствует результату работы функции fmod(double v1, double v2) математической библиотеки MSVC6, поскольку при ее выполнении используется прямой вызов данной функции в C Runtime Library. В некоторых случаях функция fmod в MSVC6 (и соответственно MathMod), выдает неверный результат.

Если в Ваших программах используется данная функция, замените вызов MathMod вызовом следующей функции, которая всегда возвращает верный результат:

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

Следует отметить, что данная особенность имеет место только для MQL4, в MQL5 расчет данной функции производится согласно математическому определению функции.


Заключение

Отметим, что приведенный перечень не является исчерпывающим, и если Вы обнаружили какую-то новую ситуацию, которая здесь не описана, посмотрите в комментариях ниже. Если решение не нашлось, сами опишите ее в комментарии, приведите участок кода и специалисты сообщества MQL4 постараются Вам помочь.