How to calculate monetary values
Why you can't use double for financial calculations
In financial calculations it is often necessary to work with fractions. For example, in some markets, the prices of options and futures may have two or even three decimal places. Some stocks may be priced in pence, so-called penny stocks. It might seem that a primitive form of doubling would be ideal for expressing prices. But this is a big and dangerous mistake!
The classic book "Efficient Java" (currently 3rd edition) does not recommend using double for monetary calculations. See "Clause 60: Avoid floating-point and double-precision numbers if exact answers are required". Joshua Bloch wrote about this earlier in another of his classic books, "Java Puzzlers" (see "Puzzle 2").
The float and double types are not suitable for the well-known monetary calculations. Valid numbers are represented in computers as binary numbers. But most decimal numbers (such as the number 0,1 or any negative 10) cannot be represented in binary form. Thus, your binary numbers are stored in approximate value. Of course, operations on such numbers result in inexact calculus. Multiply 96,835 by 10000 in Java and be surprised at 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 a great class called BigDecimal. It allows you to set the precision of the result, control rounding, and is great for multiplying and dividing fractions. But it has significant drawbacks!
Firstly, the BigDecimal class is immutable, and any operation on a BigDecimal object will create a new memory allocation. Sort of like a string object. If you need to do a complex calculation with many divisions and multiplications, you will create many BigDecimal objects that will be placed on a heap, and this will take time. The JVM will need to keep track of all these objects and this will increase the load on the garbage collector. Eventually you will reach a limit, full garbage collection will occur and the JVM will stop.
Second, operations in BigDecimal are slow, and the code for complex operations in formulas looks clumsy and awkward.
Third, there are some important things to remember when using BigDecimal: [Four common BigDecimal class errors and how to avoid them](https://blogs.oracle.com/javamagazine/post/four-common-pitfalls- bigdecimal class errors and how to avoid them). If you are unaware of these, or have forgotten about them, it can lead to nasty rounding errors or even incorrect application behaviour.
For many latency-sensitive applications, these disadvantages outweigh the advantages. If your application is not so latency critical and you want the accuracy of the calculations to be without rounding errors, BigDecimal is an excellent choice.
Using long in calculation
What can we use for calculations if bigdecimal is not suitable for our purposes? Alternatively, we can use fixed point arithmetic. In this case, the prices are of the primitive types int and long, where we have agreed to count the last two or three digits as decimal parts. For example, we write 10389, but consider it to be a price of 1.0389, which means that all the last three digits are decimal parts in this case. Once calculated, we need to convert the price into a readable form to put the result into a FIX message or serialise it into the target protocol.
Benchmarks
The benchmark allows you to feel the difference between working with different number representations. As we can see, double has a much higher throughput than BigDecimal.
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
Conclusion
There is no single solution for all cases. For some cases BigDecimal is appropriate, for others int or long, and for some specific cases double is appropriate, considering all the nuances. For example, if the price is "never" fractional.