ФИНАНСОВАЯ ОБЛАСТЬP0L3

Денежная точность: масштаб, округление и контроль потерь

Как разделить масштаб валюты, точность вычисления и округление результата, сохранив воспроизводимость финансовых сумм.

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

Задача и границы#

EDWH должен сохранить исходную денежную сумму и объяснить каждое изменение при расчёте. Точный тип хранения сам по себе не определяет, когда округлять, куда относить остаток и какой результат сверять с источником.

Здесь рассматривается контракт расчёта и контроля, а не нормативное правило бухгалтерского учёта. Курсы валют и переоценка требуют отдельной модели. Примеры синтетические; статья имеет статус knowledge и не описывает опыт автора или новую реализацию лаборатории.

Зерно и контракт суммы#

Исходное зерно - одна строка проводки конкретного документа, юридического лица и книги учёта. Ключ включает систему-источник, идентификатор документа и номер строки. Сумма неотрицательна; для знакового расчёта дебет положителен, кредит отрицателен. Валюта обязательна: складывать значения разных валют нельзя.

business_date определяет учётный период, observed_at - момент известности строки EDWH. Для повторного расчёта фиксируют оба среза, версию входных данных и версию политики округления. Новая политика не должна незаметно менять ранее сформированный результат.

Элемент контракта Что фиксировать
Исходная сумма Значение и единицу источника до преобразования
Валюта и масштаб Код, число десятичных разрядов хранения, версию справочника
Расчёт Формулу, границы входных величин, рабочую точность
Округление Шаг, режим, этап и зерно применения
Результат Единицу, диапазон, правило обработки остатка и lineage

Масштаб валюты и масштаб вычисления#

SIX публикует текущие и исторические списки ISO 4217 с данными о minor unit. Ссылку на актуальный список полезно дополнять сохранённой версией справочника. Нельзя подставлять два разряда для любой валюты или трактовать отсутствие применимого значения как нулевой масштаб.

В предлагаемом контракте отдельно задают справочную минимальную единицу, единицу исходной суммы, масштаб хранения, рабочую точность и шаг окончательного округления. Таблица валют не заменяет договорную или учётную политику расчёта.

Для целого представления связь имеет вид amount_major = amount_minor / 10^s, где s - явно заданный масштаб. Например, учебные 12345 копеек означают 123.45 RUB при s = 2. Процентная доля этой суммы может содержать дробную копейку: её нельзя заранее отбросить только потому, что проводка хранится целым числом.

Где возникает округление#

Нужно выбрать зерно: строка, документ или агрегат отчёта. Обычно sum(round(x)) и round(sum(x)) дают разные результаты. Это разные алгоритмы, а не ошибка агрегирования.

Синтетический пример: две рассчитанные строки по 0.005 RUB, окончательный шаг 0.01 RUB, режим ближайшего значения с выбором чётного при равном расстоянии (half-even). Каждая строка даёт 0.00 RUB; округление суммы 0.010 RUB даёт 0.01 RUB. При half-up обе строки дали бы по 0.01 RUB. Ни один режим здесь не объявляется обязательным для реального учёта.

Вход, RUB До 0.01, half-even До 0.01, half-up
1.005 1.00 1.01
-1.005 -1.00 -1.01
1.015 1.02 1.02

Двойное округление тоже меняет итог: 1.2451 при half-even сначала до 0.001, затем до 0.01 становится 1.24; напрямую до 0.01 получается 1.25. Промежуточное округление должно быть предусмотрено контрактом, а не случайно возникать при записи во временную таблицу.

Представление и промежуточные вычисления#

Целые минимальные единицы удобны для сложения уже зафиксированных проводок. Для долей нужны дробные промежуточные значения; диапазон надо проверять и у отдельных сумм, и у агрегатов. В SQLite sum() целых может завершиться ошибкой переполнения, а total() возвращает floating point. Замена функции ради обхода ошибки нарушает контракт точности. Документация SQLite.

В PostgreSQL 18 numeric(p,s) при записи округляет к заданному масштабу и проверяет диапазон. numeric округляет середину от нуля; это надо согласовать с политикой приложения. Двоичные real и double precision неточны. Документация PostgreSQL 18. Это описание СУБД, не результат запуска PostgreSQL-тестов.

В Python 3.14 для Decimal вход передают строкой, избегая предварительного float. Контекст задаёт рабочую точность, а quantize и выбранный режим - окончательный масштаб. Сигнал Inexact позволяет обнаружить потерю ненулевых разрядов. Документация Python.

Рабочую точность выбирают по максимальным входам и цепочке операций, а не одним универсальным числом. Например, деление может не иметь конечного десятичного результата. В предлагаемом процессе неожиданный Inexact до разрешённого этапа округления останавливает расчёт; ожидаемый на финальном этапе записывается в контроль. Точный тип не восстанавливает разряды, потерянные источником.

Остаток распределения и контроль#

Пусть 0.01 RUB нужно распределить поровну между тремя строками. Независимое округление долей до копейки даст три нуля. Если контракт требует сохранения общей суммы, можно распределять целые минимальные единицы: сначала целые части долей, затем остаток по убыванию дробных частей, при равенстве - по стабильному ключу строки. Получится одна копейка у одной строки и нули у двух других. Это предложенный алгоритм, не требование стандарта. Отрицательные суммы требуют отдельного знакового правила; например, распределять модуль, затем возвращать знак.

Псевдокод последовательности, без привязки к СУБД:

Сохранить исходные значения и версию контракта.
Проверить валюту, единицу, конечность и допустимый диапазон.
Вычислить на выбранном рабочем масштабе без скрытого округления.
Округлить только на согласованном зерне.
Применить утверждённое правило остатка, если оно требуется.
Сохранить результат, разницу и происхождение корректировки.
Сверить строки и документ в каждой валюте и временном срезе.

Контроль округления хранит исходный расчёт, результат и разницу result - unrounded, идентификатор политики и причину разрешённой потери. Для распределения проверяют точное равенство суммы долей целевой сумме. Для проводок проверяют баланс документа по валюте и книге учёта, а полноту набора проверяют отдельно. Общий ноль не исключает две компенсирующие потери.

Допуск нужен только там, где его допускает контракт, с явной единицей и областью применения. Он не должен скрывать потерянные проводки. До приёмки нужны сценарии неизвестного масштаба, смешанных валют, отрицательной середины, двойного округления, переполнения, нечислового значения, повторного расчёта и изменения политики. Для каждого заранее задают ожидаемый результат или отказ.

Связанные темы и вопросы#

Основы зерна описаны в проводках и документах, агрегирование - в финансовом SQL, полнота - в сверках. Лаборатория использует целые копейки RUB и не реализует описанную политику дробных расчётов.

  1. Какой результат должен совпасть с источником: сумма округлённых строк или округлённая сумма документа?
  2. Как доказать, что при изменении рабочей точности не изменился окончательный результат?
  3. Какие версии данных и правил нужны для повторения отчёта через год?

Источники и документация

  1. SIX - ISO 4217 currency lists and minor units ↗
  2. PostgreSQL 18 - Numeric Types ↗
  3. Python 3.14 - decimal ↗
  4. SQLite - sum and total ↗

Источники нужны для самостоятельной проверки. Примеры и интерпретации на сайте могут быть учебными упрощениями.