Thread overview
Removing the precision from double
Nov 01, 2018
kerdemdemir
Nov 02, 2018
H. S. Teoh
Nov 02, 2018
Nicholas Wilson
Nov 02, 2018
Neia Neutuladh
November 01, 2018
I have two numbers

First The price  = 0.00000016123
Second Maximum allowed precision = 0.00000001(it can be only 0.001, 0.0001, 0.00001, ..., 0.0000000001 bunch of zeros and than a one that is it)

Anything more precise than the allow precision should truncated.
So in this case 0.00000016123 should turn into 0.00000016.

I coded this which in my tests have no problem :

double NormalizeThePrice( double price, double tickSize)
{
    import std.math : floor;
    double inverseTickSize = 1.0/tickSize;
    return floor(price * inverseTickSize) *  tickSize;
}

writeln(NormalizeThePrice(0.00000016123, 0.00000001));
Returns 1.6e-07 as excepted.

I am doing trading and super scared of suprices like mathematical errors during the multiplications(or division 1/tickSize) since market will reject my orders even if there is a small mistake.

Is this safe? Or is there a better way of doing that ?

Erdemdem



November 01, 2018
On Thu, Nov 01, 2018 at 11:59:26PM +0000, kerdemdemir via Digitalmars-d-learn wrote:
> I have two numbers
> 
> First The price  = 0.00000016123
> Second Maximum allowed precision = 0.00000001(it can be only 0.001,
> 0.0001, 0.00001, ..., 0.0000000001 bunch of zeros and than a one that
> is it)

Beware!  Floating-point values in D are stored as binary, and certain decimal fractions cannot be stored exactly in binary.  Because of that, it's generally better to consider using a decimal arithmetic library for representing monetary amounts, especially when you have precise requirements on the number of decimal places a number has to fit into. IEEE floating-point has some quirks that can give you a nightmare of a debugging session if you're expecting them to behave exactly like decimals.


[...]
> I am doing trading and super scared of suprices like mathematical errors during the multiplications(or division 1/tickSize) since market will reject my orders even if there is a small mistake.
> 
> Is this safe? Or is there a better way of doing that ?
[...]

You probably want to be using a decimal number library instead of floating-point. The mismatch between the binary representation and the decimal representation will give you a headache especially if you're dealing with financial transactions that expects certain behaviours of the decimal digits, which the binary digits cannot exactly match.

Either that, or store your numbers as fixed-point integers (e.g., as an int or long with the rightmost n digits interpreted to be the fractional part -- if you go this route you'd want to encapsulate the arithmetic in a struct with overloaded operators so that it's more convenient to use, and you'll want to be careful of arithmetic overflows).

Using IEEE floating-point for financial computations is tricky business, and you need to fully understand what you're doing and exactly how IEEE floats work, in order not to get yourself into trouble at some point.


T

-- 
We've all heard that a million monkeys banging on a million typewriters will eventually reproduce the entire works of Shakespeare.  Now, thanks to the Internet, we know this is not true. -- Robert Wilensk
November 02, 2018
On Thursday, 1 November 2018 at 23:59:26 UTC, kerdemdemir wrote:
> I have two numbers
>
> First The price  = 0.00000016123
> Second Maximum allowed precision = 0.00000001(it can be only 0.001, 0.0001, 0.00001, ..., 0.0000000001 bunch of zeros and than a one that is it)
>
> Anything more precise than the allow precision should truncated.
> So in this case 0.00000016123 should turn into 0.00000016.
>
> I coded this which in my tests have no problem :
>
> double NormalizeThePrice( double price, double tickSize)
> {
>     import std.math : floor;
>     double inverseTickSize = 1.0/tickSize;
>     return floor(price * inverseTickSize) *  tickSize;
> }
>
> writeln(NormalizeThePrice(0.00000016123, 0.00000001));
> Returns 1.6e-07 as excepted.
>
> I am doing trading and super scared of suprices like mathematical errors during the multiplications(or division 1/tickSize) since market will reject my orders even if there is a small mistake.
>
> Is this safe? Or is there a better way of doing that ?
>
> Erdemdem

the function you does what you describe is called quantize[1].

However be warned that dealing with money as a floating point type is generally not a great idea. Precision losses (especially when working with ranges like 10^-7) and NaNs (Sociomantic's $9T bug, yes 9T thats what lround(NaN) gives) [2] are thing you _must_ account for.

I _think_ the accepted way of dealing with it is to use an (non overflowing[3]!) integer representing the lowest amount (e.g. 1 cent), and deal in integer multiples of that.

[1]: https://dlang.org/phobos/std_math.html#.quantize
[2]: https://dconf.org/2016/talks/clugston.html
[3]: https://dlang.org/phobos/core_checkedint.html
November 02, 2018
On Thu, 01 Nov 2018 23:59:26 +0000, kerdemdemir wrote:
> I am doing trading and super scared of suprices like mathematical errors during the multiplications(or division 1/tickSize) since market will reject my orders even if there is a small mistake.
> 
> Is this safe? Or is there a better way of doing that ?

tldr: consider using std.experimental.checkedint and highly granular units, like milli-cents. Also consider using a rational number library. Doubles aren't necessarily bad, but they'd concern me.

# 1. Fixed precision

The common way of working with sensitive financial stuff is fixed precision numbers. With fixed precision, if you can represent 0.0001 cents and you can represent ten billion dollars, you can represent ten billion dollars minus 0.0001 cents. This is not the case with single-precision floating point numbers:

    writeln(0.0001f);
    writefln("%20f", 10_000_000_000f);
    writefln("%20f", 10_000_000_000f - 0.0001f);

prints:

0.0001
  10000000000.000000
  10000000000.000000

The problem with fixed precision is that you have to deal with overflow and underflow. Let's say you pick your unit as a milli-cent (0.001 cents, $0.00001).

This works okay...but if you're trying to assign a value to each CPU cycle, you'll run into integer underflow. Like $10/hour divided by CPU cycles per hour is going to be under 0.001 cents. And if you try inverting things to get cycles per dollar, you might end up with absurdly large numbers in the middle of your computation, and they might not fit into a single 64-bit integer.

## Fixed precision with Checked

std.experimental.checkedint solves the overflow problem by turning it into an error. Or a warning, if you choose. It turns incorrect code from a silent error into a crash, which is a lot nicer than quietly turning a sale into a purchase.

It doesn't solve the underflow problem. If you try dividing $100 by 37, you'll get 270270 milli-cents, losing a bit over a quarter milli-cent, and you won't be notified. If that's deep in the middle of a complex calculation, errors like that can add up. In practice, adding six decimal places over what you actually need will probably be good enough...but you'll never know for certain.


# Floating point numbers

Floating point numbers fix the underflow and overflow problems by being approximate. You can get numbers up to 10^38 on a 32-bit float, but that really represents a large range of numbers near 10^38.

**That said**, a double can precisely represent any integer up to about 2^53. So if you were going to use milli-cents and never needed to represent a value over one quadrillion, you could just use doubles. It would probably be easier. The problem is that you won't know when you go outside that range and start getting approximate values.


# Rational numbers

A rational number can precisely represent the ratio between two integers. If you're using rational numbers to represent dollars, you can represent a third of a dollar exactly. $100 / 37 is stored as 100/37, not as something close to, but not exactly equal to, 2.702 repeating.

You can still get overflow, so you'll want to use std.experimental.checkedint as the numerator and denominator type for the rational number. Also, every bit of precision you add to the denominator sacrifices a bit of range. So a Rational!(Checked!(long, Throw)) can precisely represent 1/(2^63), but it can't add that number to 10. Manual rounding can help there.