Howdy Folks,
One particularly nettle-some issue of various exchanges that we have encountered centers around the issue of dealing with numerical precision.
This issue has at least four main conspirators:
1. Ordinary round-off issues. As you're probably aware, when calculating the actual quantities of the coins involved in a trade you're likely going to encounter the need to do some rounding. For example, if the price of a trading pair is 3 Coin A / Coin B, and you have 1 Coin A, you can buy a whopping 0.33333333333333333333.... coin B.
2. How do you display it? How many decimal points shall we display? This gives us a 2nd level of fun-with-rounding because whatever rounding we've done prior may need to get rounded again to fit the display. Or perhaps we've rounded too aggressively prior and now the display shows a lot of trailing zeros after a now merely
wrong number.
3. How do you control the propagation of round-off error throughout your system? You're constantly calculating prices, adding transaction amounts together, and trying to present the information on reports. There are many places for rounding issues to seep into this.
4. How do you represent these numbers internally? Are you going to use Integers? Floats? Strings? Tally sticks? Some other fundamentally better method?
All of this is a giant nuisance and distraction from the main goal of doing whatever else you're doing with software. Whenever you see bookkeeping numbers displayed in anybody's software, they've had to grapple with these issues, with whatever degree of success they've had.
My purpose in this post is not to gloat over their discomfort. Instead, I'd like to explain how
bookwerx deals with these issues.
The party starts with internal representation. I'll spare you the gory details about why Ints, Floats, Strings, and Tally sticks are not sufficient for this job. Instead, I'll assert that
bookwerx gets this done by using two integers to store every number. These integers are called the significand and the exponent and they work very similarly to scientific notation. For example, the tuple (70000523, -7)
really does a completely adequate job of representing
exactly 7.0000523.
In this system we still have the issue of MAXINT to be wary of. But that's a headache for developers and is of no concern to the end-user, who never sees these numbers in this form anyway. In actual practice, integer MAXINT is high enough to accommodate the actual numbers in real use. For example: a mere 41 bit int would be enough to describe 10000.00000001 of some coin. If you're dealing with that many BTC for example, you'll probably be able to get bigger Ints. I leave it as an exercise for the reader to figure out what could be done with 53, 64, and 128 bit Ints.
Internal representation is merely the first step. Next, one must have a standard for rounding when doing multiplication and division. Using my prior examples again, If the price of BBBAAA is exactly 3, and I have 7.0000523 AAA, I can buy 2.333350766666666.... BBB. The
bookwerx system can't represent that number directly and so you'll need to round to some power of 10. How about round it to 8 decimal places as is frequently done? Hence the result is 2.33335077, which
bookwerx can easily represent as (233335077, -8)
Regarding the issue of propagation of error, there is very little of this in
bookwerx. The numbers are stored internally in an exact format. They can easily be added and subtracted with zero loss and that's
all bookwerx cares about. Problem solved. Other people might want to perform other numerical operations on these numbers, and they'll need to figure out how to convert between this internal representation and whatever representation they use and how to control their own error propagation. But that's outside of
bookwerx' job description.
Finally, we return to the issue of display. Suppose our result is 2/3. We round this to 8 decimal places and get 0.66666667. This of course involves a one-time burst of error, but we can tolerate that and we can easily represent this internally as (66666667, -8). Later, we want to display this rounded to 3 decimal places (because that's the sort of thing that people do.) Shall we just truncate and display 0.666? Shall we round again giving us (667,-3) internally and then display 0.667 ? So many things to fret about! Even after you bang your fist on the table and
order the resolutions of these tedious details, you now have a new problem. Your end-user thinks he has 0.66666667 Quatloos, but only sees 0.667 in his account summary. Worse, he sees 0.6666 in some other transaction listing, so your system is internally inconsistent.
Eye-oh! How about dump the above and try plan B?
How about give the user control over the quantity of decimal places to display and provide some visual feedback regarding hidden-loss-of-precision-due-to-the-rounding-of-the-display? The underlying data is never touched and the reports round as necessary but let the users know that there are details omitted. Finally, because
all of the reports are drawing from the
One Source of Truth in a consistent manner,
all of the reports are internally consistent.
I have attached some screenshots to illustrate:
Start with some transactions with the display limited to 5 decimal places:
By clicking on the suitable control, the user can easily round to 4 decimal places. But now we have some red to indicate the existence of hidden detail:
By continuing this process the user can further limit the display as desired:
But wait... there's more! Why stop at zero? Do billionaires look at the 10^1 or 10^2 power of their finances? I wouldn't! Let's keep limiting the display even more...