How to calculate monetary values
Why you can't use double for financial calculations
Problem: during the work day you need to calculate prices for different stocks, securities, etc. Let's dig deeper.
In financial calculations, it is often necessary to operate with fractional numbers. For example, in some markets, prices for options and futures may have two or even three decimal places. The prices of some stocks can be in pennies, the so-called penny stocks. It would seem that the primitive type double is ideal for expressing prices in Java. But this is a big, dangerous mistake!
The classic book Effective Java (3rd edition for now) discourages using double for money. See "Item 60: Avoid float and double if exact answers are required". Joshua Bloch wrote about this earlier in another of his classic book, Java Puzzlers (see Puzzle 2).
The float and double types are not suitable for monetary calculations that are known to everyone. Real numbers are represented in computers as binary numbers. But most decimal numbers (such as the number 0.1 or any other negative pow of 10) it's impossible to represent in binary form. Therefore, your binary numbers are stored in an approximate value in the computer. Of course, operations with such "approximate" numbers lead to "approximate" values. Multiply in Java 96.835 by 10000 and be surprised by the result:
jshell> double a = 96.835 * 10000.0
a ==> 968349.9999999999
If we calculate the price in this style, someone who handles this price will be surprised.
Should I use BigDecimal everywhere?
Java has an excellent BigDecimal class. It allows you to set the precision of the result, control rounding and works great for multiplications and divisions of fractional numbers. But it has significant drawbacks!
Firstly, the BigDecimal class is immutable and every operation on the BigDecimal object will produce a new allocation with a result operation. Something like a String object. If you need to perform a complex calculation with many divisions and multiplications, you will create many BigDecimal objects that will be allocated to the heap, which takes time. The JVM will need to keep track of all these objects and will increase the load on the garbage collector. Eventually, you will reach the limit and a complete garbage collection will occur and the JVM will stop.
Second, operations in BigDecimal are slow, and the code of complex operations in formulas looks cumbersome and inconvenient.
Thirdly, there are some important points that you should remember when using BigDecimal: Four common pitfalls of the BigDecimal class and how to avoid them. If they are not known or forgotten, it can lead to nasty rounding errors or even incorrect application behavior.
For many applications that are sensitive to latency, these disadvantages are more significant than advantages. If your application is not so critical to latency, and the accuracy of the calculation should be without any rounding errors, BigDecimal is a great choice.
Using long in calculation
What can we use for calculation if Bigdecimal doesn't fit our purpose? Alternatively, we can use fixed-point arithmetic. In this case, prices represent primitive type int and long, in which we agree to consider the last two or three digits as decimal parts. For example, we write 10389, but we consider that this is 1.0389 price, i.e. any last three digits for this case are a decimal part. After the calculation, we need to convert the price into a human-readable view to place the result in FIX-message or other.
Benchmark just to feel the difference between operating different number representations. As we can see double has much more throughput over BigDecimal with zero allocation rate.
Benchmark Mode Cnt Score Error Units
Arithmetic.aDouble thrpt 5 330631192.642 ± 9359440.323 ops/s
Arithmetic.aDouble:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.aDouble:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁷ B/op
Arithmetic.aDouble:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.aLong thrpt 5 40676471.542 ± 3304307.569 ops/s
Arithmetic.aLong:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.aLong:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁶ B/op
Arithmetic.aLong:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.baseline thrpt 5 4532377823.385 ± 354747798.199 ops/s
Arithmetic.baseline:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.baseline:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁸ B/op
Arithmetic.baseline:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.bigDecimal thrpt 5 17153026.140 ± 1587551.803 ops/s
Arithmetic.bigDecimal:·gc.alloc.rate thrpt 5 2355.572 ± 218.022 MB/sec
Arithmetic.bigDecimal:·gc.alloc.rate.norm thrpt 5 144.000 ± 0.001 B/op
Arithmetic.bigDecimal:·gc.count thrpt 5 629.000 counts
Arithmetic.bigDecimal:·gc.time thrpt 5 424.000 ms
There is no single solution for all cases. For some cases, BigDecimal will do, for others int or long, and for some specific cases double will do, taking all the nuances into account. For example, if the price is "never" fractional.