Skip to content

refactor(core): execution и funding — через money.ts, Decimal уходит из сигнатур (E2) - #9

Merged
alexnikolskiy merged 1 commit into
mainfrom
perf/e2-money-execution-funding
Jul 28, 2026
Merged

refactor(core): execution и funding — через money.ts, Decimal уходит из сигнатур (E2)#9
alexnikolskiy merged 1 commit into
mainfrom
perf/e2-money-execution-funding

Conversation

@alexnikolskiy

Copy link
Copy Markdown
Contributor

Шаг E2 волны E. Значений не двигает, меняет публичные сигнатуры.

Почему эти восемь мест не перевели вместе с остальными 29

Они передавали Decimal между функциями и наружу. Приватный fillPrice отдавал полную цену исполнения, а computeOpenFill делил на неё нотионал. Замени его на div(notional, shiftBps(...)) — и цена вышла бы во float64 перед делением, а она в общем случае непредставима точно: размер поехал бы в последнем разряде.

Решение — назвать выражения целиком, а не дробить

Ровно по правилу money.ts «одна функция — одно целое выражение»:

Функция Выражение
sizeAtShiftedPrice notional / (base × (1 ± bps/1e4))
feeOnShiftedNotional base × (1 ± bps/1e4) × size × feeBps/1e4
fundingCost (rate8h / (ih·60)) × barMinutes × (size × mark) × sign

Порядок множителей в каждой повторяет прежний дословно: умножение коммутативно в математике, но не в decimal.js — он округляет до рабочей точности на каждой операции, поэтому перестановка была бы сдвигом значений.

Одно сознательное исключение

perMinuteFundingFraction остаётся на Decimal, и причина записана в коде: внутри пакета его никто не зовёт, он существует для исследовательского контура backtester'а (computeFundingPaidFraction), который накапливает им дробь по всему окну удержания, складывая сотни слагаемых. Отдай он number — каждое слагаемое округлялось бы до сложения, и отчёт о разрыве реализма поехал бы у потребителя. Шаг, который «не двигает значений», не может двигать их через границу пакета.

Доказательство, а не заявление

Рядом с новой арифметикой лежит дословная копия прежней, и обе сверяются побитово (toBe, не toBeCloseTo) на 15 000 случайных наборов: открывающий филл (цена, комиссия, размер), закрывающий филл (цена, комиссия от нотионала закрытия), фандинг за бар. Отдельно закреплён порядок проверок: непокрытый бар возвращает ноль до проверки интервала.

Что понадобится потребителю при bump'е пина

computeBarFunding теперь возвращает number. В backtester'е это два механических места — runner.ts (.toNumber() сразу после вызова) и test/funding.test.ts. Значения там те же: вызывающий и раньше немедленно схлопывал Decimal в число. Правка принадлежит шагу bump'а пина, как это было с волной C (bt#183), а не этому PR.

Гейты

  • pnpm typecheck0
  • pnpm test120 passed / 0 failed (+4 новых)
  • golden-tape (ref трасс против expected-traces.json) — зелёный ⇒ трассы не сдвинулись
  • pnpm gate:determinism → clean; pnpm gate:tapes → 3 tape(s) OK

…из сигнатур (E2)

Восемь оставшихся мест из тридцати семи. Их не перевели вместе с остальными
не по недосмотру: они ПЕРЕДАВАЛИ `Decimal` между функциями и наружу, и
покомпонентный перевод сдвинул бы значения. Приватный `fillPrice` отдавал
полную цену исполнения, а `computeOpenFill` делил на неё нотионал; замени
его на `div(notional, shiftBps(...))` — и цена вышла бы во float64 ПЕРЕД
делением, а она в общем случае непредставима точно.

Решение — не дробить выражения, а назвать их целиком, ровно по правилу
money.ts «одна функция — одно целое выражение»:

  sizeAtShiftedPrice     notional / (base × (1 ± bps/1e4))
  feeOnShiftedNotional   base × (1 ± bps/1e4) × size × feeBps/1e4
  fundingCost            (rate8h / (ih·60)) × barMinutes × (size × mark) × sign

Порядок множителей в каждой повторяет прежний дословно: умножение
коммутативно в математике, но не в decimal.js — он округляет до рабочей
точности на каждой операции, поэтому перестановка была бы сдвигом значений.

`computeBarFunding` теперь возвращает `number`. Единственный вызывающий в
движке и раньше немедленно звал `.toNumber()`.

`perMinuteFundingFraction` СОЗНАТЕЛЬНО оставлен на `Decimal` — это
единственное исключение, и причина записана в коде: внутри пакета его никто
не зовёт, он существует для исследовательского контура backtester'а, который
накапливает им дробь по всему окну удержания. Отдай он `number` — каждое
слагаемое округлялось бы до сложения, и значения поехали бы у потребителя.
А этот шаг значений не двигает по определению.

Значения проверены, а не заявлены: рядом с новой арифметикой лежит дословная
копия прежней, и обе сверяются ПОБИТОВО (`toBe`, не `toBeCloseTo`) на 15 000
случайных наборов — открывающий филл, закрывающий филл, фандинг. Плюс
сохранён порядок проверок: непокрытый бар возвращает ноль ДО проверки
интервала.

Гейты: pnpm typecheck → 0; pnpm test → 120 passed / 0 failed; golden-tape
(ref трасс против expected-traces.json) зелёный; gate:determinism и
gate:tapes чистые.
@alexnikolskiy
alexnikolskiy merged commit fd5af35 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