🧮 Финансовые расчёты в Python и Go: как не потерять деньги из-за одного нуля или кавычек
Недавно я ловил баг в функции расчёта PNL. Из-за неправильного использования типов ошибка в расчетах накапливалась сначала в виде погрешности, а потом переросло в серьезную ошибку для пользователя. Для решения нужно было просто использовать правильный тип данных для финансовых расчетов. За этим багом скрывается целый мир подводных камней, о которых я давно не вспоминал и хочу рассказать.
🐍 Python: лучший вариант работать через встроенный `decimal`
Если вам нужны расчеты до копеек (2-х знаков после запятой), то самый простой вариант использовать int. Это наиболее эффективно с точки зрения производительности, точности и хранения в БД.
Если нам нужна большая точность, то определенно наш выбор - decimal.Decimal.
На тему "почему float это плохо для точных расчетов?" написано очень много материалов. Достаточно вспомнить, что в Python:
0.1 + 0.1 + 0.1 == 0.3 # False
и больше про float не вспоминать.
Инициализация Decimal
Главное правило - создавать Decimal через строку (Decimal('0.1')) или int (Decimal(1)), а не через число с плавающей точкой (Decimal(0.1)). Этот способ убивает всю магию точности еще до создания объекта.
from decimal import Decimal
price = Decimal('19.99') # ✅ правильно
tax_rate = Decimal('0.08') # ✅ правильно
total = price * (1 + tax_rate)
print(total) # 21.5892
price = Decimal(19.99) # ❌ неправильно
tax_rate = Decimal(0.08) # ❌ неравильно
total = price * (1 + tax_rate)
print(total) # 21.58919999999999834504049723Управление округлением: метод quantize()
Главный инструмент для приведения чисел к нужной точности (например, нужно округлить до 2-х знаков после запятой). не забывайте указывать стратегию округления при необходимости.
from decimal import Decimal, ROUND_HALF_UP
value = Decimal('10.125')
value.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) # 10.13Контекст выполнения
Если нужно управления точностью расчетов во всем приложении, используйте getcontext().prec глобально при старте приложения.
from decimal import Decimal, getcontext getcontext().prec = 20 print(getcontext().prec) # 20
Если тебе нужно временно повысить точность для сложного расчета, используйте менеджер контекста with localcontext(). Это безопасно изолирует изменения от остального приложения.
from decimal import Decimal, localcontext
with localcontext() as ctx:
ctx.prec = 27
a = Decimal('123.45')
b = Decimal('6.789')
result = a * bСравнение значений
Decimal умный и сравнение с другими типами поддерживает, но такие операции могут дать непредсказуемый результат, поэтому всегда используем с Decimal другие Decimal.
Decimal("10.0") == 10.0 # True
Decimal("10.01") == 10.01 # False
Decimal("10.01") == Decimal(10.01) # False, потому что см. про инициализациюДля предсказуемости результатов в цепочках вычислений также важно использовать константы с тем же типом Decimal.
ZERO = Decimal('0')
ONE = Decimal('1')🐹 Go: точность под высокими нагрузками
В Go нет встроенного decimal. Стандартный float64 такой же неточный, как в Python. Аналогично если есть возможность использовать integer scaling, то это лучший вариант. В противном случае выбираем стороннюю библиотеку. И тут выбор встает между скоростью и удобством.
shopspring/decimal
Самая популярная библиотека - shopspring/decimal. Она простая и удобная, но под капотом использует math/big, что приводит к аллокациям в куче и доп расходам на GC, что может быть критично для high-load.
govalues/decimal
Недавно наткнулся на govalues/decimal. Она создана специально для транзакционных финансовых систем. Фишки:
- Иммутабельность - безопасна для конкурентного доступа
- Нулевые аллокации - все операции без выделения памяти в куче
- Без паник - все ошибки возвращаются явно.
- Банковское округление
- Поддержка до 19 десятичных знаков - хватит для большинства валют (и крипты)
package main
import (
"fmt"
"github.com/govalues/decimal"
)
func main() {
// Инициализация из строк — тоже безопасно
price, _ := decimal.Parse("19.99")
taxRate, _ := decimal.Parse("0.08")
// Вычисления
tr, _ := taxRate.Add(decimal.One)
total, _ := price.Mul(tr)
fmt.Println(total.Round(2)) // "21.59"
}
По бенчмаркам govalues быстрее shopspring и не создает мусора. Если в вашем сервисе много операций по расчетам - присмотритесь.