Почему нельзя использовать double для финансовых расчетов

В финансовых расчетах часто приходится оперировать дробными числами. Например, на некоторых рынках цены на опционы и фьючерсы могут иметь два и даже три знака после запятой. Цены на некоторые акции могут выражаться в пенни, так называемые penny-stocks. Для валидации ордеров перед отправкой на рынок надо проверять, не выходит ли указанная цена ордера за определенные пределы (price limits), рассчитанные от текущей рыночной цены инструмента. Например, надо вычислить +/-5% от цены 1.005. Казалось бы для выражения цены в Java идеально подходит примитивный тип double. Но это крупная, опасная ошибка!

Классическая книга «Effective Java» (вышло уже 3-е издание) совершенно справедливо не рекомендует использовать double для денег. См. «Item 60: Avoid float and double if exact answers are required«. Еще раньше Джошуа Блох писал об этом в другой своей классике «Java Puzzlers» (см. Puzzle 2).

Типы float и double не годятся для денежных расчетов. Вещественные числа представлены в компьютерах в виде двоичных чисел. Но большинство десятичных чисел (например, число 0,1 или любую иную отрицательную степень числа 10) невозможно точно представить в двоичной форме. Поэтому в компьютере ваши двоичные числа хранятся в приблизительном значении. Разумеется, и операции с такими «приблизительными» числами приводят «приблизительным» значениям. Умножьте в Java 96.835 на 10000 и удивитесь результату:

jshell> double a = 96.835 * 10000.0
a ==> 968349.9999999999

или классика: от 2 рублей отнять 1 руб. 10 копеек, сколько получится сдачи:

jshell> double a = 2.00 - 1.10
a ==> 0.8999999999999999

Представляете, как такой код будет работать в кассовом аппарате или в разменном автомате?

BigDecimal спешит на выручку

Для точной арифметики в Java есть отличный класс BigDecimal. Он позволяет задавать точность результата управлять округлением и работает отлично при умножениях и делениях дробных чисел. Но у него есть существенные недостатки!

Во-первых, этот класс неизменяемый (immutable), а значит каждая операция над объектом BigDecimal порождает новый объект BigDecimal с результатом операции. Точно также, например, операции над String порождают новые объекты String. Если вам требуется провести сложное вычисление с множеством делений и умножений, вы создадите много объектов класса BigDecimal, которые будут аллоцированы в heap, на что требуется время. JVM надо будет отслеживать все эти объекты, и увеличит нагрузку на сборщик мусора. В конце концов вы достигните лимита и произойдет полный сбор мусора с остановкой JVM.

Второй недостаток, операции в BigDecimal выполняются медленно, а код сложных операций в формулах выглядит громоздко и неудобно.

При работе с BigDecimal следует помнить о некоторых очень важных нюансах: Four common pitfalls of the BigDecimal class and how to avoid them. Если о них не знать или забыть, это может привести к неприятным ошибкам округления или вообще неверному поведению приложения.

Как видите, недостатков больше, чем достоинств. Если ваше приложение не так чувствительно к latency, то BigDecimal — отличный вариант. В моей практике было написание приложения для HR для вычисления зарплат и пенсий сотрудников банка, и там требование к latency было не критичным, а точность вычисления требовалась высокая, без всяких ошибок округления. BigDecimal использовался там без вопросов.

Примитивы int и long помогут там, где не поможет BigDecimal

Но ведь с ценами все же надо как-то работать? Что же использовать, если не BigDecimal? Как вариант можно воспользоваться арифметикой с фиксированой точкой. Здесь цены выражаются с помощью примитивов int и long, в которых мы условливаемся считать последние два или три знака — знаками после запятой. То есть пишем 96835, а считаем что это 96.835, т.е. любое последние три знака для данного конкретного случая — знаки после запятой. Тогда все деления будут целочисленными без дробных частей, все умножения будут тоже целочисленными без всяких странных бесконечных девяток. Просто при отображении цены для людей или для конвертации в текстовое FIX-сообщение мы переводим число в строку и ставим десятичную точку перед третьим знаком с конца

На некоторых биржах (так, например, было в реализации FIX на Сиднейской фьючерсной бирже до 20 марта 2017 года) для этого используется так называемый price multiplier. То есть вы в FIX сообщении с биржи получаете число в поле «цена» 56789 для какого-то инструмента, а price multiplier в спецификации этого инструмента, например, «100» говорит вам, где в этой цене стоит десятичная точка. Значит цена равна «567.89». И то же самое преобразование надо сделать, когда вы отправляете на биржу сообщения. Здесь, кстати, еще одна польза — экономится 1 байт при передаче из-за того, что выбрасывается точка из цены.

Главный недостаток этого подхода — это трудности с написанием кода. Надо постоянно держать в голове это условие про позицию фиксированной точки в числе, не забывать о ней и не перепутать с другими переменными, которые выражают например количество (qty).

Выводы

Единого решения для всех случаев нет. Для каких-то случаев подойдет BigDecimal, для каких-то — int или long, а для каких-то конкретных — double с учетом всех нюансов. Например, если цена «никогда» не бывает дробной. Это бывает при торговле акциями, но не деривативами, где на некоторые экзотические инструменты цена бывает равна 0.0 или даже отрицательной!

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s