Skip to content

refactor(core): единая точка прохода для денежной арифметики - #7

Merged
alexnikolskiy merged 1 commit into
mainfrom
perf/money-chokepoint
Jul 28, 2026
Merged

refactor(core): единая точка прохода для денежной арифметики#7
alexnikolskiy merged 1 commit into
mainfrom
perf/money-chokepoint

Conversation

@alexnikolskiy

Copy link
Copy Markdown
Contributor

Точка прохода для денежной арифметики — то, о чём владелец спросил: «не получится ли, что потом опять надо учитывать нюансы; хотелось постоянный механизм».

Проблема

new Decimal(...) стоял россыпью в 37 местах пяти файлов. Правила денежного счёта не были записаны нигде — они держались на том, что кто-то их помнит. Поэтому любая правка по деньгам начиналась с археологии, а смена представления означала 37 правок и 37 шансов ошибиться.

Значения не двинулись

Это извлечение, а не оптимизация. golden-tape.test.ts — байт-идентичность замороженных ожиданий — проходит без переморозки. 112 тестов зелёные, gate:determinism и gate:tapes чистые.

Именно поэтому шаг сделан отдельно от правок по производительности: после него они становятся правкой одного файла с уже готовым доказательством, что рефакторинг ничего не сдвинул.

Главное правило: одна функция — одно целое выражение

Это не стилистика, а требование точности, и оно неочевидно. Выражение

new Decimal(cash).plus(gross).minus(fee).toNumber()

держит полную десятичную точность на всей цепочке и выходит во float64 ровно один раз. Сборка из примитивов — sub(add(cash, gross), fee) — даёт два выхода, и промежуточный результат округляется до ближайшего double раньше времени.

Поэтому здесь нет универсальных add/sub, из которых можно собрать что угодно: каждая функция повторяет одно конкретное выражение движка целиком. Само правило закреплено тестом, который показывает расхождение на живом примере — если оно когда-нибудь исчезнет, тест об этом скажет.

Что переведено, что нет

Файл Мест Статус
portfolio.ts 20 переведён
protection.ts 5 переведён
risk.ts 4 переведён
execution.ts 5 нет
funding.ts 3 нет

execution.ts и funding.ts не просто считают — они передают Decimal между своими функциями и наружу: fillPrice возвращает Decimal, а computeOpenFill делит на него в полной точности; computeBarFunding тоже отдаёт Decimal. Перевести их, не сдвинув значений, можно только вместе со сменой публичных сигнатур, то есть это правка контракта пакета. Смешивать её с этим шагом значит потерять его главное свойство. Причина записана в заголовке модуля, а не только здесь.

Заготовлено на будущее

Брендированные типы Price/Qty/Cash объявлены, но не применены: их протаскивание через OpenPosition, OpenFill, Trade затрагивает публичные типы контракта. Отдельная правка, чисто типовая, с нулевым эффектом в рантайме — типобезопасность Nautilus без аллокации, которой избавлялась ethers.js (разбор — control-center docs/analysis/22).

Шкала вынесена в SCALE как параметр, а не рассыпана константой по коду, — чтобы переход на per-instrument tick size (precisionMode: TICK_SIZE у ccxt) был правкой этого файла, а не вторым заходом по всем местам.

Гейты

  • tsc --noEmit0
  • vitest run112 passed (было 100; +12 тестов на правила модуля)
  • gate:determinism — clean
  • gate:tapes — 3 tape(s) OK
  • golden'ы — не переморожены

🤖 Generated with Claude Code

До этого `new Decimal(...)` стоял россыпью в 37 местах пяти файлов, и правила денежного счёта не
были записаны нигде — они держались на том, что кто-то их помнит. Любая правка по деньгам
начиналась с археологии, а смена представления означала 37 правок и 37 шансов ошибиться.

ЗНАЧЕНИЯ НЕ ДВИНУЛИСЬ. Это извлечение, а не оптимизация: golden-tape тест на байт-идентичность
замороженных ожиданий проходит без переморозки, 112 тестов зелёные, gate:determinism и gate:tapes
чистые. Именно поэтому шаг сделан ОТДЕЛЬНО от правок по производительности — после него они
становятся правкой одного файла с готовым доказательством.

ГЛАВНОЕ ПРАВИЛО МОДУЛЯ: одна функция — одно целое выражение. Это не стилистика, а требование
точности, и оно неочевидно: `Decimal(cash).plus(gross).minus(fee).toNumber()` выходит во float64
ровно один раз, а сборка из примитивов `sub(add(cash, gross), fee)` — два раза, и промежуточный
результат округляется до ближайшего double раньше времени. Поэтому универсальных примитивов, из
которых можно собрать что угодно, здесь нет; есть составные операции под конкретные выражения
движка. Само правило закреплено тестом, который показывает расхождение на живом примере.

Переведены portfolio.ts (20 мест), risk.ts (4), protection.ts (5). Не переведены execution.ts и
funding.ts: они передают Decimal между функциями и наружу, поэтому их перевод — это смена
публичных сигнатур пакета, а не извлечение арифметики. Причина записана в заголовке модуля.

Брендированные типы Price/Qty/Cash объявлены, но не применены: их протаскивание через OpenPosition,
OpenFill и Trade затрагивает публичные типы контракта. Отдельная правка, чисто типовая, с нулевым
эффектом в рантайме (control-center docs/analysis/22).

Шкала вынесена в SCALE как параметр, а не константа по коду, — чтобы переход на per-instrument
tick size был правкой этого файла.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexnikolskiy
alexnikolskiy merged commit 38283b7 into main Jul 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant