spec: refine performance rules from implementation decisions (WealthLedger/main#54)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 13s

This commit is contained in:
2026-11-08 14:21:09 +03:00
parent 5625c52b29
commit 5c11c71a19
7 changed files with 109 additions and 66 deletions
@@ -58,7 +58,7 @@ Issue: WealthLedger/main#54
сбой индекса превращается в `INDEX_HISTORY_UNAVAILABLE` внутри
`benchmark`/`reference`.
- Числа: деньги — `MoneyDto` (RUB, десятичная строка, округление до копеек
half-even); доли (`Rate`) — десятичная строка до 6 знаков, `0.151` = 15,1 %;
half away from zero, как общий decimal helper); доли (`Rate`) — десятичная строка до 6 знаков, `0.151` = 15,1 %;
точки `series` — десятичные RUB до копеек.
- Идемпотентность: GET без побочных эффектов, кроме догрузки истории индекса.
Её повтор безопасен (upsert по индексу и торговому дню).
@@ -93,19 +93,22 @@ Issue: WealthLedger/main#54
### Переводы между счетами в `all`
T-Bank не передаёт в операции счёт-контрагента. Поэтому перевод
определяется парой операций.
T-Bank не передаёт в операции счёт-контрагента. Совпадение операций —
лишь кандидат на перевод, а не подтверждение. Не объявлять кандидата
внутренним потоком без независимого признака связи.
- **Деньги.** Вывод на счёте A и пополнение на счёте B (оба входят в
результат) с равной суммой RUB и `occurredAt` в пределах 5 минут.
Пара исключается из потоков. Без пары операция остаётся внешним потоком.
- **Деньги.** Вывод на счёте A и пополнение на счёте B равной суммы RUB
в пределах 5 минут — только кандидат. Пока нет подтверждения связи,
оба счёта исключаются из расчётной части `all` с причиной
`UNCONFIRMED_TRANSFER`. Без кандидата операция остаётся внешним потоком.
- **Бумаги.** `OUTPUT_SECURITIES` / `TRANS_*` на счёте A и
`INPUT_SECURITIES` / `TRANS_*` на счёте B с тем же инструментом и
количеством в тот же день по Москве. Пара внутренняя. Без пары оба счёта
получают `UNVALUED_SECURITIES_TRANSFER` и исключаются.
количеством в тот же день — только кандидат; оба счёта исключаются.
Для одиночного ввода или вывода применяется `UNVALUED_SECURITIES_TRANSFER`.
Правило парности нужно подтвердить на реальных данных при первом запуске
(см. Assumptions).
В текущем контракте T-Bank нет признака, подтверждающего пару. Полностью
корректное включение счетов с переводами в `all` отложено до появления
подтверждённой связи; не вычислять процент на предположении.
### Причины недоступности
@@ -118,6 +121,8 @@ T-Bank не передаёт в операции счёт-контрагента
нет вычислимой стоимости (то же правило, что у `portfolio-value-history`).
4. `NON_RUB_ACTIVITY`.
5. `UNVALUED_SECURITIES_TRANSFER`.
6. `UNCONFIRMED_TRANSFER` (только `all`, когда есть кандидат на внутренний
перевод без доказанной связи).
Как причины попадают в ответ:
@@ -126,15 +131,22 @@ T-Bank не передаёт в операции счёт-контрагента
- В `all` исключённые счета перечисляются в `excludedAccounts`, а
`completeness = incomplete`. Если не осталось ни одного счёта, ответ
`data: null`, а `meta.unavailable` содержит все счета.
- `NO_SOLUTION` бывает только в `return` (пользователя или индекса) и не
- `NO_SOLUTION` и `AMBIGUOUS_RETURN` бывают только в `return`
(пользователя или индекса) и не
исключает счёт.
- Если даты последних полных оценок оставшихся счетов `all` в Москве
различаются, сводка целиком `data: null` с причиной
`ACCOUNT_VALUATION_DATES_DIFFER` в `meta.unavailable` для каждого
затронутого счёта: суммы разных дат не складываются.
- `INDEX_HISTORY_UNAVAILABLE` бывает только в `benchmark` / `reference`.
### Расчёт
- **Период.** `from` — `historyFrom` (в `all` — минимальный по счетам
результата). `to` — дата по Москве самого раннего `fetchedAt` среди
счетов результата. `days = to − from` в календарных днях.
- **Период.** `from` — дата первого внешнего потока по Москве (решение
Change Owner: не дата открытия счёта). Полнота истории по-прежнему
проверяется от `historyFrom`. `to` — дата по Москве `fetchedAt`; для `all` она общая
только когда все включённые счета оценены в один московский день.
`days = to − from` в календарных днях.
`basis = annual` при `days ≥ 365`, иначе `period`.
- **Агрегаты.** `netInvested = Σ потоков`,
`currentValue = Σ totalsByCurrency[RUB]`,
@@ -143,8 +155,10 @@ T-Bank не передаёт в операции счёт-контрагента
- **XIRR.** Найти r из `Σ CFᵢ / (1 + r)^((dᵢ − d₀) / 365) = 0`, где
пополнение входит как `−`, вывод — как `+`, а `currentValue` — как `+` в
дату `to`. d₀ — дата первого потока. Корень ищется на `(−0.9999, 100]`.
- Нет смены знака или корень не найден с точностью |NPV| ≤ 0,01 ₽ →
`NO_SOLUTION`.
- Нет корня или корень не найден с точностью |NPV| ≤ 0,01 ₽ →
`NO_SOLUTION`. Если решений более одного — `AMBIGUOUS_RETURN`,
произвольный корень не выбирается. Смена знака на концах диапазона
не доказывает единственность решения.
- Нет потоков → `NO_SOLUTION`.
- Для `basis = period` значение равно `(1 + r)^(days / 365) − 1`.
- **Теневой портфель.** Для каждого потока берётся закрытие индекса на дату
@@ -168,12 +182,15 @@ T-Bank не передаёт в операции счёт-контрагента
- Хранится (индекс, торговый день по Москве) → точное CLOSE, upsert.
Нерабочие дни не хранятся.
- **Свежесть.** Догрузка нужна, если последняя сохранённая дата меньше
`to`, а последняя попытка была больше часа назад. На каждый индекс
- **Свежесть.** Расширение диапазона назад загружается сразу; хвост до
`to`, где закрытия ещё нет, перепроверяется не чаще раза в час после
успешной проверки; повтор после неудачной попытки — не чаще раза в час.
На каждый индекс
одновременно идёт не больше одной догрузки, повторные чтения ждут её
результат. Первая загрузка начинается с `from − 10 дней`, чтобы было
закрытие для потока в выходной.
- **Достаточное покрытие.** Есть закрытие `≤` даты первого потока и
- **Достаточное покрытие.** Есть закрытие `≤` даты первого потока,
подтверждённый диапазон доходит до даты последнего потока и
последнее закрытие `≥ to − 10` календарных дней. 10 дней покрывают
новогодние каникулы биржи. Иначе `INDEX_HISTORY_UNAVAILABLE`.
- **Лицензия.** ISS бесплатно отдаёт данные для ознакомления. Приложение
@@ -186,7 +203,7 @@ T-Bank не передаёт в операции счёт-контрагента
| --- | --- | --- | --- |
| Нет сессии / нет подключения / счёт не найден или недоступен | Доступ | 401 / 503 `TBANK_NOT_CONNECTED` / 404 | Существующие сценарии счёта |
| Причина недоступности счёта | Бизнес | 200, `data: null` или `excludedAccounts` | Исчезает после синхронизации или изменения данных |
| XIRR не сходится | Бизнес | 200, `return.unavailableReason = NO_SOLUTION` | Нет |
| XIRR не сходится или неоднозначен | Бизнес | 200, `return.unavailableReason = NO_SOLUTION` либо `AMBIGUOUS_RETURN` | Нет |
| MOEX недоступен, сохранённой истории хватает | Технический | 200, полный результат | Догрузка при следующем чтении (не чаще раза в час) |
| MOEX недоступен, истории не хватает | Технический | 200, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series: []` | То же |
| Сбой хранилища или непредвиденная ошибка | Технический | 500 Problem Details | Повтор из UI; другие вкладки не затронуты |