Skip to content

perf(core): квантизация — на границу артефакта, а не после каждой операции (C2) - #6

Merged
alexnikolskiy merged 1 commit into
mainfrom
perf/c2-quantize-at-boundary
Jul 28, 2026
Merged

perf(core): квантизация — на границу артефакта, а не после каждой операции (C2)#6
alexnikolskiy merged 1 commit into
mainfrom
perf/c2-quantize-at-boundary

Conversation

@alexnikolskiy

Copy link
Copy Markdown
Contributor

Шаг C2 инициативы engine-hot-path-optimization. Несёт также инженерную половину C4 — bump версии, потому что refresh-expectations этого прямо требует.

Что и почему

core/ звал quantize после каждой арифметической операции: 6–12 раз на бар, каждый раз полным кругом new Decimal(n).toDecimalPlaces(8).toFixed() → строка → Number. Самая дорогая статья профиля бара.

Смысл правки — не «ускорить вопреки дизайну», а привести код к своему же дизайну. Детерминизм покупался на гранулярности бара, а наблюдаем он только на границе артефакта: canonicalJson.serialize квантует каждое число в любом случае. Пербарная подрезка не давала артефакту ничего, чего не даёт сериализация, — она лишь меняла последующую арифметику.

Decimal-арифметика сохранена. Её замена на целочисленную фиксированную точку — отдельный шаг C3, отданный на решение владельца по объёму.

Версии

ENGINE_VERSION 0.0.00.1.0 — изменилась семантика исполнения. traceFormatVersion остался 1: форма трейса не тронута, а конфляция двух версий воссоздала бы проблему, которую решение (A) уже закрыло. Пакет 0.1.10.2.0.

Переморозка оправдана не тем, что тесты позеленели

Сдвиг измерен differential-харнессом на замороженных лентах (backtester#181), сверявшимся с состоянием до всей серии перф-волн — и до этого бампа версии, чтобы отделить численный эффект от смены engineVersion в самом трейсе.

Сценарий Структурных расхождений Сдвинулось чисел Из скольких Макс. отн. сдвиг
baseline.json 0 13 177 5.09e-11
universe-multi.json 0 83 515 2.16e-10
variant.json 0 44 442 4.49e-10

Последовательность решений, состав ордеров и сделок, индексы баров и метки времени — совпали. Двинулись только величины, в десятом значащем разряде. Пример: trades[0].size 89.33037948 → 89.330379475452.

Машинный вердикт харнесса: ПЕРЕМОРОЗКА РАЗРЕШЕНА (код возврата 0).

Перф

profile-runner, trusted-зонд, 20 000 баров, taskset -c 2,3, min из 6, заморозка контекста снята:

мкс/бар
до C2 36.1
после C2 24.3

×1.49, −33%. Одна чистая пара — стенд был занят почти всё время замера, и гейт из backtester#177 отклонил остальные попытки. Порядок величины согласуется с долей decimal.js в профиле (30.8%).

Тест, который стал строже

Юнит-тест на размер закрепляет теперь обе половины инварианта: внутри движка значение полное (9.995002498750624), а на границе даёт ровно то число, которое раньше подрезалось пербарно (9.9950025). Без второй половины тест разрешал бы молча потерять квантизацию совсем.

Гейты

  • tsc --noEmit0
  • vitest run100 passed
  • gate:determinism — clean (no ambient sources in src/)
  • gate:tapes — 3 tape(s) OK

Что дальше

Потребитель (backtester) поднимает пин и перемораживает свои golden'ы отдельным PR — форма артефактов не изменилась, но traceRef каждого прогона другой.

🤖 Generated with Claude Code

…рации (C2)

`core/` звал `quantize` после каждой арифметической операции: 6–12 раз на бар, каждый раз полным
кругом `new Decimal(n).toDecimalPlaces(8).toFixed()` → строка → `Number`. Это самая дорогая статья
профиля бара.

Смысл правки — не «ускорить вопреки дизайну», а привести код к своему же дизайну. Детерминизм
покупался на гранулярности бара, а наблюдаем он только на границе артефакта: `canonicalJson`
квантует КАЖДОЕ число в любом случае. То есть пербарная подрезка не давала артефакту ничего, чего
не даёт сериализация, — она лишь меняла последующую арифметику. Теперь симуляция считает в полной
точности `Decimal`, а 8 знаков появляются один раз, при записи.

Decimal-арифметика сохранена: её замена на целочисленную фиксированную точку — отдельный шаг C3,
отданный на решение владельца по объёму.

`ENGINE_VERSION` 0.0.0 → 0.1.0: изменилась семантика исполнения. `traceFormatVersion` остался `1` —
форма трейса не тронута, а конфляция двух версий воссоздала бы проблему, которую решение (A) уже
закрыло. Пакет 0.1.1 → 0.2.0.

ПЕРЕМОРОЗКА ОЖИДАНИЙ ОПРАВДАНА НЕ ТЕМ, ЧТО ТЕСТЫ ПОЗЕЛЕНЕЛИ. Сдвиг измерен differential-харнессом
на замороженных лентах (backtester#181), сверявшимся с состоянием ДО всей серии перф-волн и ДО
этого бампа версии — чтобы отделить численный эффект от смены engineVersion в самом трейсе.
Три сценария: ноль структурных расхождений, максимальный относительный сдвиг 4.49e-10,
последовательность решений, состав ордеров и сделок, индексы баров и метки времени совпали.

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

typecheck 0; vitest 100 passed; gate:determinism clean; gate:tapes 3 tapes OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexnikolskiy
alexnikolskiy merged commit 28e1c1c 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