Comments 50
Спасибо за статью, вы копнули явно глубже, чем я когда-то в свое время. Интересно было так же, что добавили сюда же порядок крипты, а выбор решений подкрепили ссылками на "кто еще так делает".
А фраза "Это помогает избежать проблем с JavaScript на фронтенде" мне напомнила анекдот (слегка адаптированный) про то, "как я стал миллионером": сначала я купил одно яблоко, продал его и купил два, а потом сделал перевод денег через микросервис, написанный на JS :)
Интересно, спасибо. Многого не знал (правда, со многим и не сталкивался). А по округлениям не планируете написать? Например, накопленные проценты по вкладам: если суммировать за каждый день, то сумма будет одна, а если сразу посчитать за период - другая. Распространенная ситуация с депозитами
Пример из российской практики: до 29 февраля 2004 года использовался код валюты RUR (810), а после деноминации был введен RUB (643). Интересно, что в некоторых legacy-системах до сих пор можно встретить старый код.
Дорогой ИИ, у тебя снова галлюцинации. Выпей вот эти таблетки.
И более того, код 810 используется при формировании лицевых счетов клиентов в рублях в банках:
40702810 - юрлица
42301810 - депозит до востребования ФЛ
Ну и т.д.
Вообще-то, 20-значный номер счета это не просто набор цифр. Он разбит на группы.
Первые 3 цифры - "счет первого порядка" (не часто, но иногда используется при отборе счетов)
Первые 5 цифр - "счет второго порядка". Используется для определения типа счета
Далее (6-8 цифры) - цифровой код валюты. И вот там для рублей так и используется 810.
В данном случае "810" - это не код валюты.
По этому вопросу есть разъяснения ЦБ РФ
Разъяснение Банка России от 09.11.2017 "По вопросу, связанному с обозначением признака рубля в номере лицевого счета"
Потому и используется, что уже огромное количество счетов ЦБ разрешил не перенумеровывать. При этом Единые казначейские счета бюджетные в Банке России открываются уже с 643 кодом в 6-8 разрядах номера счета.
Спасибо за комментарий! Понимаю, что может выглядеть сомнительно. Я взяла эту информацию из статьи в блоге Газпромбанка: https://habr.com/ru/companies/gazprombank/articles/659675/
Там есть следующая цитата: "В нашей стране есть еще легаси-код валюты — RUR (код 810) – обозначение нашей валюты до 29.02.2004. Именно до этого времени планировали полностью провести деноминацию и завершить работу со старой валютой."
Соберу побольше информации на этот счет и отредактирую.
В некоторых банках (не буду указывать пальцем), в API до сих пор требуется указывать код валюты 810 при создании платежа.
Нет никаких галлюцинаций, 810 это цифровой код ISO для уже недействующей валюты RUR.
например, на момент написания статьи евро использовалось в 84 странах
Наверное всё же в 24... 20 стран еврозоны и 4-5 микрогосударств.
я бы ещё дополнил тем, что валюты могут менять не только разрядность вследствие деноминации, но и порядок дробления, например, британский фунт до 1971 года делился на 240 пенни, а после - на 100
Совершенно не рассматривается исторический ракурс. А именно - COBOL на котором до сих пор работает огромное количество финансового кода.
Также, на западе в финансовых кругах имеет определенное распространение платформа IBM i (AS/400), которая ориентирована именно на коммерческие расчеты. И где БД (DB2) глубоко интегрирована в саму систему (является неотъемлемой ее частью).
И вот там есть два формата данных с фиксированной точкой - decimal и numeric. И основным языком разработки (более 80% кода на нем пишется) на этой платформе является RPG (писал про него раз и два) который поддерживает нативно все типы данных, использующиеся в БД. Типу decimal там соответствует тип packed (фактически это BCD, в расширениях C и C++ на этой платформе есть поддержка типов _Decimal(n,p) и _DecimalT<n,p> соответственно). Типу numeric в RPG соответствует тип zoned - фактически это строковое представление числа. Но являющееся числовым типом данных.
Все финансовые вычисления ведутся именно с этими типами данных (обычно - decimal/packed, numeric/zoned используется там, где требуется легко и просто конвертировать число в строку и обратно).
Плюс поддержка в RPG арифметики с округлением (контролируется поведение последней значащей цифры при присвоении результат с большей точностью в переменную с меньше точностью).
Как эти типы хранятся в памяти можно посмотреть тут
есть ещё один хороший способ хранить деньги:
сделать отдельные 2 колонки с целыми числами, отдельно для целой части, отдельно для дробной части * 1000000000.
Используется в Тинькофф, tinkoff api
Я использую float64 т.к. мне не важны копейки :-)
Если речь идет об аналитических системах (OLAP) и отчетах для ЛПР-ов (PowerBI) - полностью согласен. Там округление до тысяч часто считается более чем приемлемым, на графиках и диаграммах всё равно эти погрешности будут в суб-пиксельных масштабах, даже на самых 8K-панелях. А вот использовать float-ы (хоть 32, хоть 64, хоть сколько угодно) - можно только если нужно что-то приблизительное. Нейросети (с дальнейшим квантованием), данные с адронного коллайдера и прочих телескопов Hubble - там оправдано. Для того и придуманы. А деньги - ну... это действительно сложно. Но IMHO в основе должны быть только целые материальные числа.
Во-первых в апи тинька, по крайней мере, в питоне, все операции с деньгами идут в Quotation, и гоняются оттуда в Decimal и обратно. Во-вторых в float64 однажды вы поймаете проблему цифры 0.3, которая будет появляться в совершенно рандомных местах, ломая всю логику, и выставляя заявку на 291.454658274359234 лотов, и получая гарантированную ошибку от API.
А с каких это пор в c++ появился тип decimal? Опять статья-фантазия от ИИ?
В виде расширений может быть. Например, в реализациях С/С++ на платформе IBM i, как писал выше, есть типы _Decimal(n,p) и _DecimalT<n,p> Основаны на BCD.
В стандарте - нет.
Ну в таком случае можно и любой third party за уши притянуть.
Пока что единственное, чего нет в стандарте, но что поддерживается всеми современными компиляторами - это pragma once. Остальное же не переносимо, о чём надо явно указывать. Точно так же как в Borland c++ была куча всего, чего и близко нет в c++.
Там речи о переносимости вообще не идет. Потому что у этой системы сколько всяких возможностей, которых и близко нет у других. И что-то с нее куда-то переносить никто в здравом уме не будет.
Там, к примеру, можно SQL запросы непосредственно в С/С++ код вставлять. В том числе и статические, с подготовкой запроса на этапе компиляции.
SQL запросы в си++ точно так же: либо хардкод строк либо сторонние библиотеки. К си++ это отношения не имеет. Статью надо исправлять, чтобы она была корректной. Иначе складывается впечатление что её писал ИИ, который не проверяет факты (что типично для того же чатгпт).
Я в одном из камментов специально отметил что нужно рассматривать всесторонне. Есть платформы, специально предназначенные для крупных коммерческих и финансовых систем. И там не надо сову на глобус натягивать - есть и удобные средства для эффективно работы с БД без привлечения всяких сторонних библиотек и нативные типы данных для работы с финансами.
И ситуация описанная в камменте ниже
Так и случилось в один прекрасный момент несколько лет назад, когда путешествия на Бали (смотри курс индонезийской рупии IDR) стали внезапно стоить сущие центы из-за переполнения.
там просто невозможна - переполнение вызывает системное исключение "Receiver value too small to hold result" мимо которого вы ну никак не пройдете.
И убытки, которые
просто списали, никого не наказали
это как раз результат "совы на глобусе", помноженной на недальновидность разработчика.
Что-то опять теплое с мягким попадали. Начали про хранение данных а перешли на использование в API. Удачи автору в работе с денежными единицами которые он в базе данных сохранит в виде строки. Начать с того что не ясно что является разделителем целой и дробной части: в US “.”, Россия, Германия и иже с ними «,».
Проблему хранения работы вроде дано решили: используют складирование: храним в рубли как 0.0001 копейку. Чтобы не было проблем в АРИ используем составной тип :(код валюты, сумма, фактор складирования), все просто и все работает.
Ну хранение связано с использованием. И что там за API не так интересно, как то, что внутри этих API - как реализована работа.
Обычно есть счет, есть валюта счета, есть баланс счета и есть справочник валют где описаны все параметры валюты, включая текущий курс.
И да, уже отмечалось, что балансы хранятся в миноритарных единицах.
В .NET например есть понятие CultureInfo.InvariantCulture. Не идеально, и естественно всех кейсов не покрывает, но хоть что-то
Интересно было бы почитать про бухгалтерские системы Зимбабве в период гиперинфляции...
Нельзя хранить баланс в миноритарных единицах, иначе будет невозможно посчитать сложные проценты с необходимой точностью для кредитов и вкладов (а почти любой банк сейчас начисляет процент на остаток), где годовая ставка пересчитывается в дневную. Если не хранить достаточную дробную часть, даже при текущей ставке 10 рублей на вкладе навсегда останутся 10 рублями т.к. дневной прирост будет ~0.00547945205, и в конце месяца вам придёт 0 вместо 16 копеек.
Честно скажу, не знаю как это реализуется. Но как-то реализуется.
Я с деньгами не работаю, процентный, тарифный, лимитный модули - это другие команды. Я по комплаенсу и клиентским данным, там не про деньги.
Скорее всего, там отдельно данные хранятся. Типа накопленных процентов по вкладу. В таблице счетов только текущие балансы и холды. А на текущем балансе или холде дробных частей миноритарных единиц быть не может.
Вообще все это в банке намного сложнее (и там всего намного больше), чем это представляется со стороны.
Скорее всего, там отдельно данные хранятся
Ну и смысл хранить данные в нескольких форматах, если можно в одном?
А на текущем балансе или холде дробных частей миноритарных единиц быть не может
Может, только слой отображения выкидывает несущественную часть
Ну и смысл хранить данные в нескольких форматах, если можно в одном?
Потому что это разные сущности. Текущий баланс по счету - это то, сколько на нем доступно для операций.
Проценты по депозиту начисляются не ежедневно а обычно раз в месяц. Они копятся отдельно и в дату капитализации зачисляются на счет. Мешать все в одну кучу - создавать самому себе лишние проблемы.
Я, конечно, не сильно в курсе как там в депозитах устроено. Но с таблицей счетов иногда работать приходится в рамках решения наших задач. Нет там никаких дробных частей в балансах.
Может, только слой отображения выкидывает несущественную часть
Какой слой отображения, в о чем сейчас? Это АБС, центральный сервер. Там не никакого отображения.
У нас не банк, но есть биллинг, который списывает абонентку ежедневно (при тарифе ххх руб/месяц).
У нас реализовано это так: Считаем сколько абонентки должно быть списано с 1 числа по сегодня, смотрим сколько уже было списано, разницу округляем до копеек и списываем. Таким образом в БД суммы хранятся с точностью до копеек, за месяц абонентка списывается точно по тарифу. Флуктуации +- копейка в ежедневных транзакциях в течение месяца никого не волнуют.
Просто хранят в минотарных, деленных на коэффициент 10 в степени (сколько надо). Например, один недавно появившийся банк хранит деньги в формате 1/10000 копейки. И все вполне работает.
Помню, при разработке биллинг-системы мы на проекте брали за базовую единицу "пикокопейку" (10^-12 копейки), и записывали в базу как bigint. Подсчёты все были сделаны тоже на основе BigInt на golang без перехода на float и базовые типы. Всё потому, что расчеты велись каждый час и стоимость была, к примеру, 0.0000137 р/час за ГБ или как-то так и никто не знал, как в будущем будут меняться цены. При таком подходе биллинг был адски точным.
мне кажется, просто нельзя пытаться начислять проценты на вклады в 10 рублей :))))
в этом случае у вас будет интересный разговор с регулятором.
конечно же это должно быть сделано на уровне регулятора. что-то вроде закона о минимальном вкладе, в котором будет прописано, что проценты на сумму меньше 100 р., к примеру, начисляться не должны.
Могу сказать как краевед, который считал эти копейки много лет (и как математик).
Ни в коем случае нельзя считать проценты на остаток добавляя какую-то дополнительную точность (условно 3 знака после запятой): всё разъедется через некоторое время с вероятностью 100%.
Правильное вычисление выглядит так:
Нужно каждый раз считать полную сумму с нужной итоговой разрядностью «как бы заново» и вычитать предыдущее значение.
Условно, если у нас на вкладе 10.00 рублей и ставка, скажем, 16.5%, и прошло, условно, 17 дней месяца, то должно быть добавлено 10.00 * 16.5 * 17 / 100 / 365 (или 366 в високосный год).
Вот эта штука 10.00 * 16.5 * 17 / 100 / 365
, если в результате нужно округлить к целому по математическим правилам, считается точно при помощи несложной арифметики в целых числах.
Получится 0.08 (8 копеек). Значит после 17 дней, у нас 10.08.
После 18 дней слова будет 10.08. А вот уже после 19-го дня будет 10.09 рублей.
Такие расчёты гарантируют, что не будет накопления ошибок округления, и на любой конкретный день сумма будет правильной.
Добавлю примеров в копилку из своего опыта, где работал сам:
- Яндекс (Яндекс.Касса). Decimal base units: Java BigDecimal
на бэкенде, DECIMAL
в базе данных (Postgres), строковое представление в API.
- Ingenico e-Payments / Worldline (четвёртый в мире Payment Gateway, самый большой в Европе). Integer with minor units: Java BigInteger
на бэкенде, BigInt
в базе данных (Oracle), где minor units = 1/1000000. В API то же самое, только minor units = 1/100 (для большинства валют) или 1/1000 (для определённого списка, но в документации об этом ни слова, просто костыли в коде).
- Банк ING (электронный кошелёк Yolt). Сам затащил подход «как в Яндексе», так как мог влиять на принятие решений. Стек тоже «как в Яндексе».
- Maersk (морские грузоперевозки). String Base Units: сериализация / десериализация из / в Java BigDecimal
на бэкенде, хранение в виде строк в базе данных (MongoDB). API использует строки.
Забавный факт про Integer with minor units:
Большие суммы: Int64 может переполниться при работе с крупными транзакциями
Если уж Int64 может переполниться, то что тут говорить про int32 (Java int
), который был до перехода на BigDecimal
(а, возможно, где-то в коде каких-то сервисов ещё и есть) в Ingenico e-Payments. Так и случилось в один прекрасный момент несколько лет назад, когда путешествия на Бали (смотри курс индонезийской рупии IDR) стали внезапно стоить сущие центы из-за переполнения. Распродажу Лавочку прикрыли только к утру, когда заметили невероятную популярность клиента Agoda и отелей на Бали, и до исправления просто прекратили (на полгода) принимать платежи в валюте IDR. Убытки, как водится, просто списали, никого не наказали.
Некоторые языки (например, Go) требуют привyедения к типу
Float64
для выполнения арифметических операций.
На сколько вижу в документации, тип big.Int имеет методы Add/Mul/Div/Mod и т.д. Поэтому не совсем понимаю, зачем необходимо конвертировать во float64?
Тем более, что вычисления с плавающей точкой неизбежно приводят к накоплению ошибки. См. рекуррентное соотношение Мюллера. Форматы с фиксированной точкой более устойчивы (хотя тоже "разбегаются", но не так быстро как форматы с плавающей точкой).
А я правильно понимаю, что для банков нет смысла связываться с minor units?
Например, для японской цены количество знаков после запятой - 0. У клиента осталась одна йена, на которую начисляетс процент. В БД нужно сохранить 1.00000005 JPY.
Если и так и так придется хранить дробные числа, то зачем банкам Integer?
Или все же есть смысл вводить некий виртуальный коэффициент, не привязанный к стандарту?
Банки не используют integer. Банки используют форматы с фиксированной точкой где указывается количество знаков после запятой. Т.е. decimal(n, p) или numeric(n, p).
И проценты [обычно] начисляются не каждый день. Сумма процентов накапливается отдельно и в назначенный день капитализируется (переводится на баланс счета). Но это отдельная операция всегда - начисление процентов.
И там много разных условий. Проценты могут начисляться на минимальный остаток по счету за месяц, например. Тогда отслеживается только сумма минимального остатка и в дату капитализации он нее исчисляются проценты (с округлением до миноритарной единицы).
Или на остаток по счету на дату капитализации.
Или еще как-то - все это прописано в условиях счета.
Как хранить деньги в базах данных и почему это не так просто, как кажется