MQL4 - automated forex trading   /  

Статьи

Cтатьи  Примеры  Особенности работы с числами типа double в MQL4 Авторизуйтесь или зарегистрируйтесь , чтобы добавить новую статью


Эта статья о возможностях
MetaTrader 4

Мобильный трейдинг!
Купите лицензию и торгуйте мобильно

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

Введение

При программировании на языке 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 постараются Вам помочь.

Создана: 02.11.2009  Автор: Eugene Irinyakov
Предупреждение: все права на данные материалы принадлежат MetaQuotes Software Corp. Полная или частичная перепечатка запрещена.
Спать или не спать?
Спать или не спать?

Предлагается альтернатива использованию функции Sleep() при реализации пауз между действиями эксперта. Рассматриваемый подход позволяет более рационально использовать машинное время.

Библиотека матричной алгебры LibMatrix (часть первая)
Библиотека матричной алгебры LibMatrix (часть первая)

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

10 комментариев  Авторизуйтесь или зарегистрируйтесь
Quantum писал(а):
Так что лично мне больше нравится первый вариант :)

Блин. Тоже молодец, поленился протестировать.

double MathModCorrect(double a, double b, int precisionRatio = -5)
{ 
   double a_ = a;
   double correction = MathMin(MathAbs(a), MathAbs(b))*MathPow(10, precisionRatio);
   
   if (a_ > 0) a_ += correction;
   else        a_ -= correction;
   
   int tmpres = a_/b;
   return (a - tmpres*b);
}
03.11.2009 12:15 TheXpert

Вообще говоря, диапазон вот такой должен быть:

наименование формата single-precision double-precision
длина числа, бит 32 64
смещенная экспонента (E), бит 8 11
остаток от мантиссы (M), бит 23 52
смещение 127 1023
формула расчета денормализованных чисел F =(-1)S∙2(E -126)∙ M/223 F =(-1)S∙2(E -1022)∙M/252
формула расчета нормализованных чисел F =(-1)S∙2(E-127)∙(1+ M/223) F =(-1)S∙2(E-1023)∙(1+M/252)
минимальное число ±2-149≈ ±1,40129846∙e-45 ±2-1074≈ ± 4,94065646∙e-324
максимальное число ±2127∙(2-2-23) ≈ ± 3,40282347∙e+38 ±21023∙(2-2-52) ≈ ± 1,79769313∙e+308


У разработчиков ошибка в описании. Они его наверное взяли из описания С, так же как и все это делают. Но реальность другая.

03.11.2009 11:48 HideYourRichess
notused писал(а):

Диапазон возможных значений чисел double гораздо шире: -1.7*e-308 до 1.7*e308

Неверная формулировка (раз уж "диапазон" и -1.7*e-308)
Спасибо. Скорректировал.
03.11.2009 09:37 Quantum
HideYourRichess писал(а):

PS. но в целом, статья нужная, да.


Да, статья жизненная :) Хотя все это давно известно, но вспомнить и увидеть практически было очень интересно.
03.11.2009 08:23 zhorzh

Диапазон возможных значений чисел double гораздо шире: -1.7*e-308 до 1.7*e308

Неверная формулировка (раз уж "диапазон" и -1.7*e-308)
03.11.2009 00:57 notused
TheXpert писал(а):

Позанудствую.

double MathModCorrect(double a, double b, int precisionRatio = -5)
{ 
   double correction = MathMin(MathAbs(a), MathAbs(b))*MathPow(10, precisionRatio);
   
   if (a > 0)  a += correction;
   else        a -= correction;
   
   int tmpres = a/b;
   return (a - tmpres*b);
}
А вообще штука нужная. Чуть бы поменьше сумбура и побольше четкости в изложении, было бы совсем хорошо.

Спасибо. А сумбур получился из-за того, что это заметка типа коллажа различных вопросов.

Насчет добавления погрешности, пусть даже небольшой, то я бы не советовал.

Вот например,

#include <stdlib.mqh>  

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

double MathModCorrect2(double a, double b, int precisionRatio = -5)
{ 
   double correction = MathMin(MathAbs(a), MathAbs(b))*MathPow(10, precisionRatio);
   
   if (a > 0)  a += correction;
   else        a -= correction;
   
   int tmpres = a/b;
   return (a - tmpres*b);
}  

int start()
  { double c1,c2;
 
    c1=MathModCorrect1(5.3,2.0);
    c2=MathModCorrect2(5.3,2.0);
    Alert("MathMod1(5.3,2.0): Output:",c1,", 8 digits precision:"+DoubleToStr(c1,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c1,15));
    Alert("MathMod2(5.3,2.0): Output:",c2,", 8 digits precision:"+DoubleToStr(c2,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c2,15));

    c1=MathModCorrect1(18.5,4.2);
    c2=MathModCorrect2(18.5,4.2);
    Alert("MathMod1(18.5,4.2): Output:",c1,", 8 digits precision:"+DoubleToStr(c1,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c1,15));
    Alert("MathMod2(18.5,4.2): Output:",c2,", 8 digits precision:"+DoubleToStr(c2,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c2,15));

    c1=MathModCorrect1(14.5,3.5);
    c2=MathModCorrect2(14.5,3.5);
    Alert("MathMod1(14.5,3.5): Output:",c1,", 8 digits precision:"+DoubleToStr(c1,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c1,15));
    Alert("MathMod2(14.5,3.5): Output:",c2,", 8 digits precision:"+DoubleToStr(c2,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c2,15));

   // один из случаев с ошибкой в стандартной MathMod
    c1=MathModCorrect1(5.0,0.1);
    c2=MathModCorrect2(5.0,0.1);
    Alert("MathMod1(5.0,0.1): Output:",c1,", 8 digits precision:"+DoubleToStr(c1,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c1,15));
    Alert("MathMod2(5.0,0.1): Output:",c2,", 8 digits precision:"+DoubleToStr(c2,8)+
          ", 15 digits precision:"+ DoubleToStrMorePrecision(c2,15));
   
   return(0);
  }

Выведет:

MathMod1(5.3,2.0): Output:1.3, 8 digits precision:1.30000000, 15 digits precision:1.300000000000000
MathMod2(5.3,2.0): Output:1.3, 8 digits precision:1.30002000, 15 digits precision:1.300020000000000

MathMod1(18.5,4.2): Output:1.7, 8 digits precision:1.70000000, 15 digits precision:1.699999999999999
MathMod2(18.5,4.2): Output:1.7, 8 digits precision:1.70004200, 15 digits precision:1.700042000000000

MathMod1(14.5,3.5): Output:0.5, 8 digits precision:0.50000000, 15 digits precision:0.500000000000000
MathMod2(14.5,3.5): Output:0.5, 8 digits precision:0.50003500, 15 digits precision:0.500035000000000

MathMod1(5.0,0.1): Output:0, 8 digits precision:0.00000000, 15 digits precision:0.000000000000000
MathMod2(5.0,0.1): Output:0, 8 digits precision:0.00000100, 15 digits precision:0.000001000000000

Так что лично мне больше нравится первый вариант :)
02.11.2009 22:48 Quantum
HideYourRichess писал(а):

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

Ни как нет. На самом деле процессор поддерживает машинные операции с числами типа extended (собственно говоря это родной тип данных математического сопроцессора). У него "19 significant digits, exponent -4932 to +4932" - это 80битное число, в отличии от 64битного типа double, о котором Вы пишите.

Спасибо. Скорректировал.

Вы правы, с extended (например, на Delphi) мы бы получили результат -4951 вместо -324 с double.

02.11.2009 21:46 Quantum

Позанудствую.

double MathModCorrect(double a, double b, int precisionRatio = -5)
{ 
   double correction = MathMin(MathAbs(a), MathAbs(b))*MathPow(10, precisionRatio);
   
   if (a > 0)  a += correction;
   else        a -= correction;
   
   int tmpres = a/b;
   return (a - tmpres*b);
}
А вообще штука нужная. Чуть бы поменьше сумбура и побольше четкости в изложении, было бы совсем хорошо.
02.11.2009 20:44 TheXpert

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

просто

...в некоторых случаях функция fmod в MSVC6 (и соответственно MathMod), выдает неверный результат.

Йоу! Так держать!

02.11.2009 17:41 fibollinger

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

Ни как нет. На самом деле процессор поддерживает машинные операции с числами типа extended (собственно говоря это родной тип данных математического сопроцессора).

У него "19 significant digits, exponent -4932 to +4932" - это 80битное число, в отличии от 64битного типа double, о котором Вы пишите.

Ирония ситуации состоит в том, что С, на котором написан МТ, просто напросто не поддерживает этот тип данных.

Соответственно и МТ его не поддерживает - а очень жаль, это наиболее точное число из доступных для нативной обработки процессором.


PS. но в целом, статья нужная, да.

02.11.2009 16:51 HideYourRichess
10 комментариев