Removing the precision from double

Neia Neutuladh neia at ikeran.org
Fri Nov 2 01:00:26 UTC 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.


More information about the Digitalmars-d-learn mailing list