Библиотеки: CDouble & CDoubleVector

 

CDouble & CDoubleVector:

Библиотека для проведения общих методов округления, используемых в разработке MQL-приложений, примитивный класс-оболочка для значений типа double и вектор для объектов CDouble. Совместима с MQL5 и MQL4!

Версия 1.02: (2018.02.18)

  • Исправлена ошибка, из-за которой округленный результат отклонялся от ожидаемого результата. Выражаю благодарность AMRALI.

Версия 1.01:

  • Исправлена ошибка, из-за которой арифметические операторы не возвращали округленные значения.
  • Добавлен метод для установки символа после вызова конструктора.


Автор: nicholi shen

 

Попробуйте снять ограничение

Note: Only one arithmetic operator can be used per statement.

И попробуйте сделать операторы сравнения "статическими".

 
fxsaber:

Попробуйте снять ограничение

И попробуйте сделать операторы сравнения "статическими".


Операторы не могут быть объявлены "статическими".

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

CDouble foo = 3, bar = 4, spam = 3;

CDouble error   = foo+bar+spam; //ERROR

CDouble error_too = (pi2 + pi5)+pi2; //ERROR

CDouble correct = foo+(bar+spam);// OK!

 

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

CDouble sum, foo=3, bar=2, spam=1;
sum = foo.AsRawDouble() + bar.AsRounded() + spam.AsRoundedTick();
Print(sum.ToString());
//6
 
nicholishen:

Операторы не могут быть объявлены как "статические".

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

class CDouble2 : public CDouble
{
private:
  static CDouble2 TmpDouble;
  
public:
  const CDouble2* const operator +( const double Value ) const
  {
    CDouble2::TmpDouble = this.m_value + Value;
    
    return(&CDouble2::TmpDouble);
  }
  
  const CDouble2* const operator +( const CDouble2 &Other ) const 
  {
    return(this + Other.m_value);
  }
  
  static CDouble2* const Compare2( const double Value )
  {
    CDouble2::TmpDouble = Value;
    
    return(&CDouble2::TmpDouble);
  }
  
  static CDouble2* const Compare2( const CDouble2 &Other )
  {
    CDouble2::TmpDouble = Other;
    
    return(&CDouble2::TmpDouble);
  }  
};

static CDouble2 CDouble2::TmpDouble;

#define _CP(A) CDouble2::Compare2(A)

#define  PRINT(A) Print(#A + " = " + (string)(A));

void OnStart()
{
  CDouble2 foo = 3, bar = 4, spam = 3;
  CDouble2 error   = foo+bar+spam + foo+bar+spam; //OK!
  
  PRINT(error.ToString()); // 10
  PRINT(_CP(foo + error + 5) > 2);
  PRINT(_CP(25) > foo + bar + 7 +spam);
  PRINT((foo + bar + spam + 9).ToString());
  PRINT((_CP(9) + foo).ToString());
  PRINT(foo + 7 > 11)
}

Результат

error.ToString() = 20
_CP(foo+error+5)>2 = true
_CP(25)>foo+bar+7+spam = false
(foo+bar+spam+9).ToString() = 19
(_CP(9)+foo).ToString() = 12
foo+7>11 = false
 
fxsaber:

Результат


Это очень умно, и мне это очень нравится, но это слишком умно для большинства пользователей... (нас обоих обвиняли в этом на форумах ;) Я зафиксирую ваши изменения в своей личной библиотеке, и другие тоже могут, но для блага большей базы пользователей я собираюсь сохранить простоту и придерживаться официальной рекомендации вызывать один из методов геттера. ( например, num1.AsRounded() * num2.AsRounded() + num3.AsRounded() )


FWIW Мне лично нравится (num1*num2+num3).AsRounded()


Проблемы с CDouble2, как было предложено:

void Func(double param)
{
}

void OnStart()
{
   CDouble2 foo = 2, bar = 3;
   double number = foo+bar; //ERROR
   Func(foo+bar); //ERROR
}
 

* Версия 1.01:

  • Исправлена ошибка, из-за которой арифметические операторы не возвращали округленные значения.
  • Добавлен метод symbol setter для установки символа после вызова конструктора
 

Здравствуйте, nicholishen. Я тестировал вашу библиотеку в течение некоторого времени. Она отличная и делает округление цен и партий легкой работой.

Но у меня есть некоторые опасения по поводу точности ваших методов округления. Я обнаружил множество ошибок округления в функциях RoundToStep(), RoundToStepUp(), RoundToStepDown() и RoundToTick(). Эти ошибки всегда возникают при округлении чисел, которые оканчиваются на 5 (например, 1.12345).

Например, CDouble::RoundToStep(1.700605, 0.00001) возвращает 1.70060, вместо правильного результата 1.70061.

Уравнение round(number / point) * point должно быть исправлено на round(number * power) / power, где и point, и power являются производными от количества десятичных цифр, до которых вы хотите округлить.

Потому что значение 1 пункта, которое должно быть = 0.00001, на самом деле кодируется как 0.0000100000000000000008180305391403130954586231382563710 в 64-битной системе с плавающей точкой двойной точности. Это приводит к тому, что конечный результат, полученный методом округления round(number / point) * point, очень часто отклоняется от правильного результата на 1 пункт (0.00001).

Кроме того, чтобы выполнить правильное "арифметическое" округление (округление средней точки от нуля), хорошим методом является добавление или вычитание полуэпсилона в качестве поправки. (Это компенсирует любое округление от половины до четности, которое было применено процессором, как предписано спецификациями IEEE-754, особенно в крайних случаях средней точки).

Функция mql's NormalizeDouble() корректно решает все эти вопросы, и вам следует использовать ее для выполнения правильного "арифметического" округления.

Здесь также приведен исходный код одной функции, которую я написал для выполнения арифметического округления, и вы можете проверить ее самостоятельно. Эта функция дает точно такие же результаты, как и NormalizeDouble(). Моя функция работает даже быстрее и поддерживает более высокий уровень точности округления. (В MQL функция NormalizeDouble() ограничена 8 десятичными цифрами).

/**
 * Округление средней точки от нуля ("арифметическое" округление)
 * Использует полуэпсилон для коррекции. (Это компенсирует IEEE-754
 * округление от половины до четности, которое было применено в крайних случаях).
 */
double RoundCorrect(double num, int precision) {
        double c = 0.5 * DBL_EPSILON * num;
// double p = MathPow(10, precision); //slow
        double p = 1; while (precision--> 0) p *= 10;
        if (num < 0)
                p *= -1;
        return MathRound((num + c) * p) / p;
}
 

Кроме того, вот скрипт, который можно использовать для отладки точности округления в библиотеке CDouble. Надеюсь, вы найдете его полезным для себя.

#property strict

#define  PRINT(A) Print(#A + " = ", (A))
#define  forEach(element, array)  for (int __i = 0, __max = ArraySize((array)); __i < __max && ((element) = array[__i]) == (element); __i++)

#include "CDouble.mqh"

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
string DoubleToFixed(double number, int decimals = 55) {

        return StringFormat(StringFormat("%%#.%if", decimals), number);
}

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void OnStart() {

        // проверка округления в некоторых крайних случаях
        double numbers_3[] = {1.005, 2.175, 5.015, 16.025};
        double numbers_6[] = {1.011885, 1.113325, 1.143355, 1.700605};

        double num;

        forEach (num, numbers_3) {

                Print("----------------------------------------------");
                PRINT( num );
                // сравните 3 функции (округлите до 2 цифр)
                PRINT( CDouble::RoundToStep(num, 0.01) );
                PRINT( NormalizeDouble(num, 2) );
                PRINT( RoundCorrect(num, 2) );
        }

        forEach (num, numbers_6) {

                Print("----------------------------------------------");
                PRINT( num );
                // сравните 3 функции (округлите до 5 цифр)
                PRINT( CDouble::RoundToStep(num, 0.00001) );
                PRINT( NormalizeDouble(num, 5) );
                PRINT( RoundCorrect(num, 5) );
        }

        // Причина проблем с округлением в библиотеке CDouble
        Print("----------------------------------------------");

        PRINT( DoubleToFixed(0.01, 55) );      // 0.0000100000000000000008180305391403130954586231382563710
        PRINT( DoubleToFixed(0.00001, 55) );   // 0.0100000000000000002081668171172168513294309377670288086

        // сравните NormalizeDouble и RoundCorrect на основе точного равенства
        Print("----------------------------------------------");

        PRINT( NormalizeDouble(numbers_6[0], 5) == RoundCorrect(numbers_6[0], 5) );  // true
        PRINT( NormalizeDouble(numbers_6[0], 4) == RoundCorrect(numbers_6[0], 4) );  // true
        PRINT( NormalizeDouble(numbers_6[0], 3) == RoundCorrect(numbers_6[0], 3) );  // true
        PRINT( NormalizeDouble(numbers_6[0], 2) == RoundCorrect(numbers_6[0], 2) );  // true
        PRINT( NormalizeDouble(numbers_6[0], 1) == RoundCorrect(numbers_6[0], 1) );  // true

}

 
amrali:

Здравствуйте, nicholishen. Я тестировал вашу библиотеку в течение некоторого времени. Она отличная и делает округление цен и партий легкой работой.

Но у меня есть некоторые опасения по поводу точности ваших методов округления. Я обнаружил множество ошибок округления в функциях RoundToStep(), RoundToStepUp(), RoundToStepDown() и RoundToTick(). Эти ошибки всегда возникают на крайних числах, которые оканчиваются на 5 (например, 1.12345).

Например, CDouble::RoundToStep(1.700605, 0.00001) возвращает 1.70060, вместо правильного результата 1.70061.

Уравнение round(number / point) * point должно быть исправлено на round(number * power) / power, где и point, и power являются производными от количества десятичных цифр, до которых вы хотите округлить.

Потому что значение 1 пункта, которое должно быть = 0.00001, на самом деле кодируется как 0.0000100000000000000008180305391403130954586231382563710 в 64-битной системе с плавающей точкой двойной точности. Это приводит к тому, что конечный результат, полученный методом округления round(number / point) * point, очень часто отклоняется от правильного результата на 1 пункт (0.00001).

Кроме того, чтобы выполнить правильное "арифметическое" округление (округление средней точки от нуля), хорошим методом является добавление или вычитание полуэпсилона в качестве поправки. (Это компенсирует любое округление от половины до четности, которое было применено процессором, как предписано спецификациями IEEE-754, особенно в крайних случаях средней точки).

Функция mql's NormalizeDouble() корректно решает все эти вопросы, и вам следует использовать ее для выполнения правильного "арифметического" округления.

Здесь также приведен исходный код одной функции, которую я написал для выполнения арифметического округления, и вы можете проверить ее самостоятельно. Эта функция дает точно такие же результаты, как и NormalizeDouble(). Моя функция работает даже быстрее и поддерживает более высокий уровень точности округления. (В MQL функция NormalizeDouble() ограничена 8 десятичными цифрами).

Спасибо, что указали на это. Я обновлю код, чтобы использовать NormalizeDouble вместо round.

step * NormalizeDouble(number_to_round / step, 0)
 
amrali:

Здравствуйте, nicholishen. Я тестировал вашу библиотеку в течение некоторого времени. Она отличная и делает округление цен и партий легкой работой.

Но у меня есть некоторые опасения по поводу точности ваших методов округления. Я обнаружил множество ошибок округления в функциях RoundToStep(), RoundToStepUp(), RoundToStepDown() и RoundToTick(). Эти ошибки всегда возникают на крайних числах, которые оканчиваются на 5 (например, 1.12345).

Например, CDouble::RoundToStep(1.700605, 0.00001) возвращает 1.70060, вместо правильного результата 1.70061

Уравнение round(number / point) * point должно быть исправлено на round(number * power) / power, где и point, и power являются производными от количества десятичных цифр, до которых вы хотите округлить.

Потому что значение 1 пункта, которое должно быть = 0.00001, на самом деле кодируется как 0.0000100000000000000008180305391403130954586231382563710 в 64-битной системе с плавающей точкой двойной точности. Это приводит к тому, что конечный результат от метода округления, round(number / point) * point, очень часто отклоняется от правильного результата на 1 пункт (0.00001).

Однако с точки зрения торговли вам нужно либо 1,70060, либо 1,70061, они оба верны. Так что, возможно, вы захотите выбрать лучший вариант в соответствии с вашими торговыми операциями, а не полагаться на математическую схему округления.