Pull to refresh

Comments 50

Спасибо за статью, вы копнули явно глубже, чем я когда-то в свое время. Интересно было так же, что добавили сюда же порядок крипты, а выбор решений подкрепили ссылками на "кто еще так делает".

А фраза "Это помогает избежать проблем с JavaScript на фронтенде" мне напомнила анекдот (слегка адаптированный) про то, "как я стал миллионером": сначала я купил одно яблоко, продал его и купил два, а потом сделал перевод денег через микросервис, написанный на JS :)

Я знаю обратную историю где на фронте кривым отображением списало полтора миллиона евро и владелец счёта отъехал на скорой. Фронтенд убивает.

Интересно, спасибо. Многого не знал (правда, со многим и не сталкивался). А по округлениям не планируете написать? Например, накопленные проценты по вкладам: если суммировать за каждый день, то сумма будет одна, а если сразу посчитать за период - другая. Распространенная ситуация с депозитами

Пример из российской практики: до 29 февраля 2004 года использовался код валюты RUR (810), а после деноминации был введен RUB (643). Интересно, что в некоторых legacy-системах до сих пор можно встретить старый код.

Дорогой ИИ, у тебя снова галлюцинации. Выпей вот эти таблетки.

И более того, код 810 используется при формировании лицевых счетов клиентов в рублях в банках:

40702810 - юрлица

42301810 - депозит до востребования ФЛ

Ну и т.д.

Вообще-то, 20-значный номер счета это не просто набор цифр. Он разбит на группы.

Первые 3 цифры - "счет первого порядка" (не часто, но иногда используется при отборе счетов)

Первые 5 цифр - "счет второго порядка". Используется для определения типа счета

Далее (6-8 цифры) - цифровой код валюты. И вот там для рублей так и используется 810.

Но ведь в этом разъяснении буквально говорится, что это «признак рубля», который теперь используется только в лицевых счетах после упразднения 810 RUR как общепризнанного признака рубля

Потому и используется, что уже огромное количество счетов ЦБ разрешил не перенумеровывать. При этом Единые казначейские счета бюджетные в Банке России открываются уже с 643 кодом в 6-8 разрядах номера счета.

Спасибо за комментарий! Понимаю, что может выглядеть сомнительно. Я взяла эту информацию из статьи в блоге Газпромбанка: https://habr.com/ru/companies/gazprombank/articles/659675/

Там есть следующая цитата: "В нашей стране есть еще легаси-код валюты — RUR (код 810) – обозначение нашей валюты до 29.02.2004. Именно до этого времени планировали полностью провести деноминацию и завершить работу со старой валютой."

Соберу побольше информации на этот счет и отредактирую.

На самом деле, вы молодец. Я прочитал вашу статью и она не похожа на сгенеренную AI. Есть обычные человеческие шероховатости.

В некоторых банках (не буду указывать пальцем), в 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 копейки. И все вполне работает.

деньги в формате 1/10000 копейки. И все вполне работает.

Вы натурально пересказали, что нельзя хранить в минотарных. Конечно вполне работает, если вы 26 знаков после запятой хранить будете (не суть важно как, главное не во float)

Помню, при разработке биллинг-системы мы на проекте брали за базовую единицу "пикокопейку" (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).

И проценты [обычно] начисляются не каждый день. Сумма процентов накапливается отдельно и в назначенный день капитализируется (переводится на баланс счета). Но это отдельная операция всегда - начисление процентов.

И там много разных условий. Проценты могут начисляться на минимальный остаток по счету за месяц, например. Тогда отслеживается только сумма минимального остатка и в дату капитализации он нее исчисляются проценты (с округлением до миноритарной единицы).

Или на остаток по счету на дату капитализации.

Или еще как-то - все это прописано в условиях счета.

Sign up to leave a comment.

Articles