From f504a0f5666a84dca4df13e70be35ef2e291a501 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Sun, 8 Nov 2026 16:49:39 +0300 Subject: [PATCH 1/4] docs: investment performance vs Moscow Exchange index (WealthLedger/main#54) (#67) --- adr/023-investment-performance-vs-index.md | 93 +++++++++++++++++++++ adr/README.md | 1 + architecture/context.md | 3 +- architecture/http-api.md | 10 +++ domains/income/overview.md | 2 +- domains/portfolio/all-accounts.md | 6 ++ domains/portfolio/data-reference.md | 12 +++ domains/portfolio/investment-performance.md | 68 +++++++++++++++ domains/portfolio/overview.md | 2 + glossary.md | 52 ++++++++++-- integrations/moex.md | 29 ++++++- overview.md | 6 +- 12 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 adr/023-investment-performance-vs-index.md create mode 100644 domains/portfolio/investment-performance.md diff --git a/adr/023-investment-performance-vs-index.md b/adr/023-investment-performance-vs-index.md new file mode 100644 index 0000000..9a40b83 --- /dev/null +++ b/adr/023-investment-performance-vs-index.md @@ -0,0 +1,93 @@ +# 023. Результат инвестирования относительно индекса Мосбиржи + +**Status:** Accepted. **Date:** 2026-11-07. Дополняет [ADR +019](019-hourly-reads-and-online-instrument-reference.md) исключением для +публичной истории закрытий индексов MOEX (по образцу +[ADR 022](022-stored-instrument-classification.md)). Решение оформлено в +[Issue WealthLedger/main#54](https://git.personal.ksv741.ru/WealthLedger/main/issues/54) +и утверждённом change `specs/openspec/changes/2026-11-07-investment-performance-vs-index/` +в репозитории specs. + +## Context + +Вкладка «Результат» должна показать, как счёт или «Все счета» справились с +деньгами, которые в него внесли, и сравнить это с вложением тех же денег в +индекс. Простая доходность «стоимость / вложено − 1» искажает оценку, когда +пополнения и выводы распределены во времени. Доходность по времени (TWR) +требует истории стоимости на каждую дату потока, а её в системе нет: хранится +только последний Portfolio. + +Сравнение с ценовым IMOEX не отвечает на вопрос пользователя: IMOEX не +включает дивиденды и купоны, а эталон должен учитывать их так же, как +портфель. ADR 019 запрещает хранить справочные данные как долговременный +источник; индексы — публичные временные ряды, для которых онлайн-запрос при +каждом открытии вкладки неприемлем по задержке и лимитам внешнего сервиса. + +## Decision + +- **«Доходность» для «Результата инвестирования» — money-weighted return (XIRR)** + по внешним денежным потокам счёта и текущей оценке Portfolio. Термин + «доходность» снимается с запрета только для этого понятия; прочие понятия + «не доходность» сохраняют прежний текст. +- **Сравнение — теневой портфель в индексе.** Те же внешние потоки + конвертируются в единицы индекса по закрытию дня потока (или ближайшего + предыдущего торгового дня) и оцениваются по закрытию на дату расчёта. + Эталон — полный доходный индекс MCFTRR; XIRR индекса считается на тех же + потоках. Это идея PME по Long–Nickels; в интерфейсе — «те же деньги в + индексе Мосбиржи». +- **IMOEX — только справочный индекс.** Он показывается рядом, но не + участвует в «опережении» и не является базой сравнения. +- **Только RUB.** Счёт с потоками или стоимостью в другой валюте не + рассчитывается (причина `NON_RUB_ACTIVITY`); конвертация валют не + выполняется. +- **Период** начинается с первого внешнего потока, не с открытия счёта. + Короче года — «за N дн.» без annualization; `basis = annual` только если + деньги вложены от 365 дней. +- **Хранение закрытий индексов.** Пара (индекс, торговый день по Москве) → + CLOSE хранится локально с upsert. Это исключение из ADR 019: публичная + история индекса, не привязанная к пользователю или подключению, хранится + как долговременные данные. Остальные справочные сведения ADR 019 не + меняются. + +### Отклонённые варианты + +- **Простой ROI** (`стоимость / вложено − 1`): искажён моментами пополнений + и выводов, поэтому не сравнивается с индексом на тех же деньгах. +- **TWR (time-weighted return):** требует стоимости портфеля на каждую дату + потока; история оценок в системе отсутствует. Сохранение такой истории + вне этого решения. +- **Ценовой IMOEX как ориентир:** не учитывает дивиденды и купоны, поэтому + сравнение с портфелем, где они учтены, смещено. +- **Конвертация валют (FX):** потребовала бы исторических курсов и + отдельного решения о методике; не нужна для личного RUB-учёта. + +## Consequences + +- Результат зависит от сохранённых операций T-Bank и последнего полного + Portfolio: неполная история или неполная оценка дают `data: null` или + `completeness: incomplete` по правилам change, а не подменяются оценкой. +- Первая загрузка индекса догружает историю от `from − 10 дней`; при + недостатке закрытий появляется `INDEX_HISTORY_UNAVAILABLE`. Фоновая + синхронизация индексов не вводится. +- Исключение из ADR 019 ограничено закрытиями индексов MOEX. + +### Оговорки и условия источника + +- **Налоги.** MCFTRR при расчёте дивидендов и купонов использует ставки для + организаций, а не НДФЛ физического лица. Индекс — ориентир альтернативы, + а не точная модель налогов пользователя. Оговорка фиксируется в методике + («Как считали»). +- **Данные MOEX ISS.** ISS бесплатно отдаёт данные «для ознакомления». + Приложение личное и локальное, данные не распространяются; источник + указывается в «Как считали». +- **Переводы между счетами.** Совпадение суммы и времени операций ещё не + доказывает внутренний перевод. Пока нет подтверждённой связи, такие счета + исключаются из рассчитанной части «Все счета» с явной причиной неполноты. +- **Несколько решений XIRR.** Процент не показывается: нельзя выбрать + произвольную ставку. Прибыль в рублях остаётся видна. + +## Links + +- Issue: [WealthLedger/main#54](https://git.personal.ksv741.ru/WealthLedger/main/issues/54) +- Change: `specs/openspec/changes/2026-11-07-investment-performance-vs-index/` + (proposal, system-analysis «Расчёт» и «Индексы MOEX», design, Decisions backend 3, 5) diff --git a/adr/README.md b/adr/README.md index 6cac8d9..a98f3b5 100644 --- a/adr/README.md +++ b/adr/README.md @@ -27,3 +27,4 @@ - [020 — Evidence for hourly sync read boundaries](020-hourly-sync-verification-evidence.md) - [021 — Тихое чтение и редкие подтверждения](021-quiet-reading-and-rare-confirmations.md) - [022 — Хранимая классификация инструментов T-Bank](022-stored-instrument-classification.md) +- [023 — Результат инвестирования относительно индекса Мосбиржи](023-investment-performance-vs-index.md) diff --git a/architecture/context.md b/architecture/context.md index 522e78d..4307352 100644 --- a/architecture/context.md +++ b/architecture/context.md @@ -31,7 +31,8 @@ WealthLedger нормализует их для просмотра и сопос информацией об инструментах. [Интеграция T-Bank](../integrations/tbank.md) объясняет полномочия подключения и ограничения внешней зависимости. -[MOEX ISS](../integrations/moex.md) поставляет публичные параметры и календарь выпусков облигаций. +[MOEX ISS](../integrations/moex.md) поставляет публичные параметры и календарь выпусков облигаций, +а также дневные закрытия индексов Мосбиржи для сравнения результата. [Реестр НКР](../integrations/nkr.md) используется для поиска кредитного рейтинга конкретного выпуска; рейтинг эмитента не подставляется вместо него. Отсутствие подходящей записи и недоступность источника имеют разный смысл. Правила интерпретации этих diff --git a/architecture/http-api.md b/architecture/http-api.md index e9e6216..ce8b9a7 100644 --- a/architecture/http-api.md +++ b/architecture/http-api.md @@ -72,6 +72,16 @@ Envelope несовместим с прежними bare-array ответами: backend-generated OpenAPI; frontend получает её обновлённый snapshot отдельно. Точную семантику поведения описывает [доход](../domains/income/overview.md). +## Investment performance + +`GET /api/v1/brokerage-accounts/{accountId}/performance` и общий маршрут +`all` возвращают [результат инвестирования](../domains/portfolio/investment-performance.md) +за всё время. Недоступность расчёта — не HTTP-ошибка: ответ `200` с +`data: null` и причинами в `meta.unavailable`. Неполная сводка «Все счета» +имеет `completeness: incomplete` и `excludedAccounts`. Доли передаются +десятичной строкой (`0.151` = 15,1 %), `return.basis` различает годовую +ставку и ставку за период. Схему задаёт backend-generated OpenAPI. + ## Validation и identity Backend преобразует и проверяет входные данные на общей HTTP-границе. diff --git a/domains/income/overview.md b/domains/income/overview.md index 97c7bae..67fdac9 100644 --- a/domains/income/overview.md +++ b/domains/income/overview.md @@ -94,7 +94,7 @@ Backend обеспечивает принадлежность выплат, ва выбранного счёта и сохраняет прежнее поведение. Будущие выплаты не включаются в итоги полученного дохода. -Аналитика читает сохранённую историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог. +Аналитика читает сохранённую историю операций и не рассчитывает налоги, доходность (её показывает [результат инвестирования](../portfolio/investment-performance.md)), валютную конвертацию или общий мультивалютный итог. Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md), точная форма ответа — в [HTTP API](../../architecture/http-api.md). diff --git a/domains/portfolio/all-accounts.md b/domains/portfolio/all-accounts.md index 2a54a46..6039389 100644 --- a/domains/portfolio/all-accounts.md +++ b/domains/portfolio/all-accounts.md @@ -59,6 +59,12 @@ Backend публикует статические маршруты под `/api/ состав счетов влияет только на фактические выплаты выбранного периода. - Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта. Прогноз не включается в реализованные доходы. +- [Результат инвестирования](investment-performance.md) складывает потоки и + стоимость только рублёвых счетов с полной историей. Счёт с причиной + недоступности исключается, сводка помечается неполной. Похожие на перевод + между своими счетами операции без подтверждённой связи исключают оба счёта + (`UNCONFIRMED_TRANSFER`). Если оценки оставшихся счетов сделаны в разные + московские дни, сводка недоступна: стоимости разных дат не складываются. ## Полнота, свежесть и доступ diff --git a/domains/portfolio/data-reference.md b/domains/portfolio/data-reference.md index ca059ae..025046a 100644 --- a/domains/portfolio/data-reference.md +++ b/domains/portfolio/data-reference.md @@ -46,6 +46,18 @@ отдельного счёта не создают валютные карточки. Если `totalsByCurrency` пуст, но позиции или деньги присутствуют, это не пустой портфель. +## Результат инвестирования + +| Поле | Смысл | Ограничение | +| --- | --- | --- | +| `netInvested`, `currentValue`, `profit` | Чистые внешние потоки, сумма RUB-оценок, их разность | Только RUB, копейки | +| `return.value` | Доля XIRR (`0.151` = 15,1 %) | `null` с `unavailableReason`; при `basis = period` — за весь период | +| `benchmark`, `reference` | Те же потоки в MCFTRR и IMOEX | IMOEX только справка | +| `outperformance` | Разница долей и стоимостей с MCFTRR | `null`, если любая сторона недоступна | +| `series` | Точки торговых дней MCFTRR | Пусто без истории индекса | + +Подробнее — [результат инвестирования](investment-performance.md). + ## Идентификация и оформление инструмента UID определяет конкретный инструмент брокера и используется для перехода diff --git a/domains/portfolio/investment-performance.md b/domains/portfolio/investment-performance.md new file mode 100644 index 0000000..13c12ed --- /dev/null +++ b/domains/portfolio/investment-performance.md @@ -0,0 +1,68 @@ +# Результат инвестирования + +Вкладка «Результат» на странице брокерского счёта и в режиме +[«Все счета»](all-accounts.md) отвечает на два вопроса: + +- сколько заработали вложенные деньги; +- обогнал ли пользователь рынок. + +Результат считается за всё время истории счёта, только в рублях. Решение и +отклонённые альтернативы описаны в +[ADR 023](../../adr/023-investment-performance-vs-index.md). + +## Показатели + +| Показатель | Смысл | +| --- | --- | +| Вложено (нетто) | Сумма пополнений минус сумма выводов рублей | +| Прибыль | Текущая стоимость портфеля минус чистые вложения | +| Доходность | Ставка XIRR с учётом сумм и дат пополнений и выводов | +| Те же деньги в индексе Мосбиржи | Стоимость и доходность тех же пополнений и выводов, вложенных в индекс полной доходности MCFTRR (с дивидендами после налогов) | +| Опережение или отставание рынка | Разница доходностей в процентных пунктах и разница стоимостей в рублях | + +Ценовой индекс IMOEX не учитывает дивиденды. Он показан на графике только +для справки и в сравнении не участвует. + +Период начинается с первого пополнения, а не с открытия счёта. Если деньги +вложены меньше 365 дней, проценты показаны за фактическое число дней и не +пересчитываются в годовые. + +## Какие операции считаются потоками + +Внешние потоки — только пополнения и выводы рублей (типы T-Bank `INPUT`, +`INPUT_ACQUIRING`, `INPUT_SWIFT`, `INP_MULTI` и соответствующие `OUTPUT`). +Сделки, купоны, дивиденды, погашения, налоги и комиссии уже отражены в +текущей стоимости и потоками не являются. Учитываются только операции до +момента последней полной оценки портфеля. + +## Когда результата нет + +Число не подменяется нулём: вместо него показывается причина. + +| Причина | Когда возникает | +| --- | --- | +| `HISTORY_START_UNKNOWN` | Неизвестна дата открытия счёта | +| `OPERATIONS_HISTORY_INCOMPLETE` | История операций не получена полностью до момента оценки | +| `PORTFOLIO_VALUATION_INCOMPLETE` | Нет полной оценки портфеля | +| `NON_RUB_ACTIVITY` | Есть потоки или остатки не в рублях | +| `UNVALUED_SECURITIES_TRANSFER` | Бумаги введены или выведены без денежной оценки | +| `UNCONFIRMED_TRANSFER` | В «Все счета» есть похожие на перевод между своими счетами операции, связь которых источник не подтверждает | +| `ACCOUNT_VALUATION_DATES_DIFFER` | В «Все счета» последние оценки счетов сделаны в разные московские дни | +| `NO_SOLUTION` | Уравнение доходности не имеет решения; прибыль остаётся | +| `AMBIGUOUS_RETURN` | Потоки меняют знак больше одного раза, и единственность ставки не доказана; прибыль остаётся | +| `INDEX_HISTORY_UNAVAILABLE` | Нет истории индекса за период; прибыль и доходность остаются, сравнения нет | + +В «Все счета» счёт с причиной исключается, а результат помечается +неполным со списком исключённых счетов. Если считать не по чему, результат +недоступен целиком. + +## График + +На графике показаны ступенчатая линия чистых вложений, стоимость тех же +денег в индексе по закрытиям торговых дней, справочная линия индекса без +дивидендов и отметка текущей стоимости портфеля на конец периода. Линии +прошлой стоимости портфеля нет: такая история пока не хранится. + +Нормативное поведение задаёт change +`2026-11-07-investment-performance-vs-index` в репозитории specs, форму +ответа — [HTTP API](../../architecture/http-api.md#investment-performance). diff --git a/domains/portfolio/overview.md b/domains/portfolio/overview.md index 15f8123..16c678b 100644 --- a/domains/portfolio/overview.md +++ b/domains/portfolio/overview.md @@ -29,6 +29,8 @@ Backend получает данные через серверную самостоятельно: его ошибка или отсутствие данных не скрывает основной портфель. Полученные выплаты и прогноз не добавляются к текущей стоимости повторно и не превращают результат открытой позиции в полную доходность. +Полную доходность с учётом пополнений и выводов показывает вкладка +[«Результат»](investment-performance.md). ## Как читать экран diff --git a/glossary.md b/glossary.md index d2fc1ed..e549a8e 100644 --- a/glossary.md +++ b/glossary.md @@ -205,9 +205,10 @@ _Не использовать_: история портфеля, snapshot дл Полная оценка одного Brokerage Account в одной валюте на момент успешного обновления Portfolio; последовательность таких точек описывает изменение оценки во времени, включая неторговые дни. Не является Portfolio Snapshot -или инвестиционной доходностью; отсутствие цены позиции не приравнивается к нулю. +или [Результатом инвестирования](#результат-инвестирования); отсутствие цены +позиции не приравнивается к нулю. -_Не использовать_: снимок состава портфеля, доходность. +_Не использовать_: снимок состава портфеля. ## Client Cached Snapshot @@ -311,8 +312,47 @@ _Не использовать_: upstream dividend type, MOEX row name. Результат анализа исполненных купонов и дивидендов по счёту за календарный период включительно в часовом поясе Europe/Moscow. Сохраняет исходные валюты и знаки денежных выплат, переданных брокером. Это не налоговый расчёт, -прогноз или инвестиционная доходность; правила расчёта объясняет -[область дохода](domains/income/overview.md). +прогноз или [Результат инвестирования](#результат-инвестирования); правила +расчёта объясняет [область дохода](domains/income/overview.md). -_Не использовать_: доходность, общий доход в одной валюте, гарантированный -доход. +_Не использовать_: доходность выплат, общий доход в одной валюте, +гарантированный доход. + +## Результат инвестирования + +Прибыль, доходность и сравнение с индексом Мосбиржи для счёта или «Все +счета» за всё время в рублях. Правила описаны в +[результате инвестирования](domains/portfolio/investment-performance.md), +решение — в [ADR 023](adr/023-investment-performance-vs-index.md). + +_Не использовать_: нереализованный результат, доход. + +## Внешний денежный поток + +Пополнение (положительный поток) или вывод (отрицательный поток) рублей со +счёта. Сделки, выплаты, налоги и комиссии внешними потоками не являются. + +_Не использовать_: операция, выплата. + +## Доходность (XIRR) + +Годовая ставка, при которой дисконтированные по фактическим дням внешние +потоки и текущая стоимость в сумме дают ноль. При истории короче года +показывается доходность за период. Единственное значение термина +«доходность» для личного результата. + +_Не использовать_: доходность к погашению, рост цены позиции. + +## Те же деньги в индексе Мосбиржи + +Стоимость и доходность тех же внешних потоков, вложенных в индекс полной +доходности MCFTRR по закрытию дня потока. Ценовой IMOEX — только справка. + +_Не использовать_: бенчмарк IMOEX, теневой портфель (в интерфейсе). + +## Опережение рынка + +Разница доходности пользователя и тех же денег в индексе в процентных пунктах +и разница стоимостей в рублях; отрицательное значение называется отставанием. + +_Не использовать_: альфа. diff --git a/integrations/moex.md b/integrations/moex.md index 5e85a6f..301b674 100644 --- a/integrations/moex.md +++ b/integrations/moex.md @@ -1,6 +1,7 @@ # Интеграция с MOEX ISS -MOEX ISS дополняет данные T-Bank публичной информацией об облигациях. +MOEX ISS дополняет данные T-Bank публичной информацией об облигациях и +дневными закрытиями индексов Мосбиржи. Backend получает её по HTTPS в JSON с `iss.moex.com`; запросы содержат идентификаторы выпуска, без token T-Bank и идентификатора брокерского счёта. MOEX не определяет доступ пользователя к инвестициям и не подтверждает @@ -47,6 +48,32 @@ HTTP-запрос ограничен таймаутом. Сеть, неуспе [странице облигации](../domains/instruments/bonds.md#получение-данных-и-восстановление). Наличие успешного HTTP-ответа не гарантирует котировку без задержки. +## Индексы + +Для [результата инвестирования](../domains/portfolio/investment-performance.md) +backend читает дневные закрытия MCFTRR (полная доходность после налогов) и +IMOEX (ценовой, справка) из +`/iss/history/engines/stock/markets/index/securities/{SECID}.json` с +`history.columns=TRADEDATE,CLOSE`, постранично по `history.cursor`. +Любая некорректная страница считается сбоем: частичная история не +сохраняется. + +Закрытия и подтверждённый диапазон покрытия хранятся локально в SQLite +(исключение из ADR 019, см. [ADR 023](../adr/023-investment-performance-vs-index.md)). +Догрузка выполняется при чтении результата одной одновременной загрузкой на +индекс: расширение диапазона назад — сразу, проверка ещё не опубликованного +закрытия и повтор после сбоя — не чаще раза в час. Недоступность +MOEX не возвращается как HTTP-ошибка: при достаточной сохранённой истории +результат считается по ней, иначе сравнение помечается +`INDEX_HISTORY_UNAVAILABLE`. События лога — +`integration.moex.index_history.succeeded` и `.failed`, без данных +пользователя. + +ISS бесплатно отдаёт данные для ознакомления; приложение личное и не +распространяет их, источник указан в пояснении «Как считали». + +## Ссылки + Условия обработки источника в прогнозе закреплены в [спецификации будущих событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-upcoming-events-use-complete-source-results). Точный механизм транспорта, разбора и сопоставления реализован в diff --git a/overview.md b/overview.md index b48b568..19f7db6 100644 --- a/overview.md +++ b/overview.md @@ -41,8 +41,10 @@ WealthLedger — локальное приложение для частного Портфель отвечает на вопрос «что есть сейчас», журнал — «что исполнено», аналитика дохода — «какие выплаты уже получены», будущие события — «что -ожидается». Прогноз не подтверждает фактическое зачисление. Полученные купоны -и дивиденды не равны инвестиционной доходности или результату после налогов. +ожидается», [результат инвестирования](domains/portfolio/investment-performance.md) — +«как сработали вложенные деньги и обогнал ли я индекс Мосбиржи». Прогноз не +подтверждает фактическое зачисление. Полученные купоны и дивиденды сами по +себе не равны доходности или результату после налогов. Денежная сумма всегда относится к одной валюте. Недоступное значение отличается от нуля, а объединение счетов не означает автоматическую -- 2.54.0 From 7fbd4273baa82e06fa415d99d0eb034fcbd335c8 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Wed, 11 Nov 2026 09:19:49 +0300 Subject: [PATCH 2/4] docs: describe RUB-only behavior (WealthLedger/main#55) (#68) --- adr/023-investment-performance-vs-index.md | 6 ++-- architecture/http-api.md | 3 +- design/overview.md | 2 +- domains/accounts/overview.md | 10 +++--- domains/income/overview.md | 14 ++++---- domains/income/upcoming-events.md | 8 ++--- domains/instruments/bonds.md | 8 ++--- domains/instruments/stocks.md | 6 ++-- domains/operations/overview.md | 6 ++-- domains/portfolio/all-accounts.md | 11 +++--- domains/portfolio/concentration.md | 17 ++-------- domains/portfolio/data-reference.md | 34 +++++++++---------- .../how-to/investigate-discrepancy.md | 8 ++--- domains/portfolio/investment-performance.md | 1 - domains/portfolio/overview.md | 31 ++++++++--------- glossary.md | 19 +++++------ integrations/tbank.md | 2 +- overview.md | 8 ++--- 18 files changed, 89 insertions(+), 105 deletions(-) diff --git a/adr/023-investment-performance-vs-index.md b/adr/023-investment-performance-vs-index.md index 9a40b83..afd161e 100644 --- a/adr/023-investment-performance-vs-index.md +++ b/adr/023-investment-performance-vs-index.md @@ -37,9 +37,9 @@ индексе Мосбиржи». - **IMOEX — только справочный индекс.** Он показывается рядом, но не участвует в «опережении» и не является базой сравнения. -- **Только RUB.** Счёт с потоками или стоимостью в другой валюте не - рассчитывается (причина `NON_RUB_ACTIVITY`); конвертация валют не - выполняется. +- **Только RUB.** Пополнения и выводы в другой валюте не учитываются, + нерублёвые позиции отфильтрованы на входе; конвертация валют не + выполняется (WealthLedger/main#55). - **Период** начинается с первого внешнего потока, не с открытия счёта. Короче года — «за N дн.» без annualization; `basis = annual` только если деньги вложены от 365 дней. diff --git a/architecture/http-api.md b/architecture/http-api.md index ce8b9a7..a59000c 100644 --- a/architecture/http-api.md +++ b/architecture/http-api.md @@ -64,8 +64,7 @@ Envelope несовместим с прежними bare-array ответами: ## Income analytics `GET /api/v1/brokerage-accounts/{accountId}/income-analytics` и общий маршрут -`all` возвращают результат только выбранного периода: итоги по исходным -валютам, точные записи выплат, разбиение по инструментам и временные +`all` возвращают результат только выбранного периода: итоги в RUB, точные записи выплат, разбиение по инструментам и временные интервалы. Данных сравнения с предыдущим периодом или годом ранее в ответе нет — ни в теле, ни среди зависимостей `meta.dependencies`, роль которых ограничена значением `selected`. Публичную машинную схему задаёт diff --git a/design/overview.md b/design/overview.md index 07bcbf5..60be5d9 100644 --- a/design/overview.md +++ b/design/overview.md @@ -66,7 +66,7 @@ compatibility re-exports `shared/theme` и `shared/ui` удалены. Фоновое обновление сохраняет доступные данные с локальным индикатором. Подробные правила — в [состояниях интерфейса](../architecture/cross-cutting/ui-resilience.md). -Финансовые значения сохраняют валюту, знак и различие между нулём и +Финансовые значения сохраняют валюту (RUB), знак и различие между нулём и неизвестной суммой. Предметные представления раскрыты в [портфеле](../domains/portfolio/overview.md), [операциях](../domains/operations/overview.md) и [банковских картах](../domains/cards/overview.md). diff --git a/domains/accounts/overview.md b/domains/accounts/overview.md index 3c73766..71ab9a9 100644 --- a/domains/accounts/overview.md +++ b/domains/accounts/overview.md @@ -33,12 +33,12 @@ | Состояние `portfolioSummary` | Что оно означает | | --- | --- | -| Доступные валютные итоги | Текущие стоимости позиций и деньги сложены точно в исходной валюте | -| Доступная сводка с пустыми итогами | Портфель получен, но валютных итогов нет; это не ошибка загрузки | +| Доступный итог | Текущие стоимости позиций и деньги сложены точно в RUB | +| Доступная сводка без итога | Портфель получен, но итоговой суммы нет; это не ошибка загрузки | | `null` | Финансовая сводка этого счёта временно недоступна; метаданные и переход на счёт сохраняются | -Валюты не конвертируются и не объединяются в общую сумму. Деньги уже входят -в валютный итог; они не образуют второй самостоятельный итог. Правила +Позиции и деньги в валютах, отличных от RUB, не участвуют в итоге. Деньги уже входят +в итог; они не образуют второй самостоятельный итог. Правила неизвестных стоимостей совпадают со [справочником портфеля](../portfolio/data-reference.md). Сводка также сообщает число ненулевых обычных позиций акций и облигаций @@ -66,7 +66,7 @@ - Неизвестная ошибка: то же состояние недоступности без действия повтора. - Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов. -Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. Карточки расположены в одну колонку при любой ширине. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию. +Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. Карточки расположены в одну колонку при любой ширине. Карточка показывает название, локализованный тип, крупный итог в RUB, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию. ## Свежесть и границы diff --git a/domains/income/overview.md b/domains/income/overview.md index 67fdac9..1024db4 100644 --- a/domains/income/overview.md +++ b/domains/income/overview.md @@ -4,7 +4,7 @@ купонами и дивидендами за выбранный период». Пользователь открывает `/accounts/:accountId/income` из обзора счёта или [общий доход](../portfolio/all-accounts.md) по доступным счетам. -Экран показывает итоги по валютам и отдельную карточку каждой +Экран показывает итоги в RUB и отдельную карточку каждой включённой выплаты. Источником служат [исполненные операции](../operations/overview.md), а не изменение цены портфеля и не календарь будущих выплат. @@ -51,10 +51,10 @@ ## Какие выплаты входят в результат - Backend проверяет доступ локально и читает сохранённые операции. Фоновая загрузка должна завершить все страницы; ошибка страницы или повреждённая цепочка курсоров не подтверждают полноту. -- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход. -- Итоги, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. Система не загружает другие периоды ради сравнения полученного дохода с выбранным. +- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в RUB; отрицательные корректировки сохраняют знак. Выплаты в других валютах не участвуют. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход. +- Итоги, временные интервалы и разбиение по инструментам считаются только в RUB. Система не загружает другие периоды ради сравнения полученного дохода с выбранным. - Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется. -- Раздел «Выплаты» показывает карточки выплат в исходном порядке `entries`. В отдельном счёте каждая выплата — отдельная карточка. В режиме «Все счета» выплаты одного инструмента, дня и точного типа образуют группу, если их две и более ([правила группировки](../operations/overview.md#группировка-в-режиме-все-счета)). Группа стоит на месте первой выплаты; каждая строка счёта показывает название счёта и свою сумму со знаком. Визуальный итог группы — точная сумма по валютам по показанным выплатам; он скрыт, если полнота периода не `complete`, или у выплаты нет суммы, или сумма равна нулю. Это клиентский расчёт и не заменяет серверные итоги аналитики дохода, которые по-прежнему приходят из ответа API и не пересчитываются. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой выплате. Дополнительный запрос за составом не нужен. +- Раздел «Выплаты» показывает карточки выплат в исходном порядке `entries`. В отдельном счёте каждая выплата — отдельная карточка. В режиме «Все счета» выплаты одного инструмента, дня и точного типа образуют группу, если их две и более ([правила группировки](../operations/overview.md#группировка-в-режиме-все-счета)). Группа стоит на месте первой выплаты; каждая строка счёта показывает название счёта и свою сумму со знаком. Визуальный итог группы — точная сумма в RUB по показанным выплатам; он скрыт, если полнота периода не `complete`, или у выплаты нет суммы, или сумма равна нулю. Это клиентский расчёт и не заменяет серверные итоги аналитики дохода, которые по-прежнему приходят из ответа API и не пересчитываются. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой выплате. Дополнительный запрос за составом не нужен. ## Полнота и принадлежность выплат @@ -71,11 +71,11 @@ Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`, равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ один раз при подготовке записей дохода и использует его же для группировки. -Приоритет UID, FIGI, ticker/type и ID операции с разделением по валюте остаётся +Приоритет UID, FIGI, ticker/type и ID операции остаётся серверным правилом; клиент воспринимает ключ как непрозрачную строку. Это поле принадлежит ответу аналитики; общий журнал операций его не требует. -Backend обеспечивает принадлежность выплат, валюту и receipt count при +Backend обеспечивает принадлежность выплат и receipt count при построении entries и instrument groups из одного набора данных. Frontend доверяет типизированному contract, не восстанавливает формулу идентификации и не выполняет второй consistency validator. Полностью пустой ответ допустим. @@ -94,7 +94,7 @@ Backend обеспечивает принадлежность выплат, ва выбранного счёта и сохраняет прежнее поведение. Будущие выплаты не включаются в итоги полученного дохода. -Аналитика читает сохранённую историю операций и не рассчитывает налоги, доходность (её показывает [результат инвестирования](../portfolio/investment-performance.md)), валютную конвертацию или общий мультивалютный итог. +Аналитика читает сохранённую историю операций и не рассчитывает налоги, доходность (её показывает [результат инвестирования](../portfolio/investment-performance.md)), или итоги по нескольким валютам. Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md), точная форма ответа — в [HTTP API](../../architecture/http-api.md). diff --git a/domains/income/upcoming-events.md b/domains/income/upcoming-events.md index 76ae07b..dfee29b 100644 --- a/domains/income/upcoming-events.md +++ b/domains/income/upcoming-events.md @@ -56,8 +56,8 @@ «сумма неизвестна» и не превращается в ноль или полученные деньги. Дата дивиденда обозначена как дата реестра. Offer и погашение сохраняют своё значение без суммы выплаты. У каждой строки видны тип, -инструмент, дата и доступная сумма в исходной валюте. Список не имеет -пагинации и не конвертирует валюты. Пустой результат означает отсутствие +инструмент, дата и доступная сумма в RUB. Список не имеет +пагинации. Пустой результат означает отсутствие известных поддерживаемых событий в выбранном окне, а не доказательство отсутствия всех будущих выплат. Инструмент без поддерживаемых данных не получает искусственную строку «нет данных». @@ -70,7 +70,7 @@ несуществующая дата дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD` без замены периода и без запуска личной работы. Фильтрация на сервере предшествует сортировке и вычислению -`incomeTotalsByCurrency`, включающего только купоны и дивиденды. +итога прогноза в RUB, включающего только купоны и дивиденды. Итоги остаются в ответе API; полный экран показывает список без общего итога списка. В режиме «Все счета» события одного инструмента, даты, точного типа и `dateKind` образуют @@ -78,7 +78,7 @@ поэтому дата реестра дивиденда и иная дата того же инструмента не объединяются. Группа стоит на месте первого события. Полный список группирует все полученные события; превью обзора сначала берёт пять событий и группирует только их. Каждая строка счёта остаётся отдельной и показывает название -счёта и свою прогнозную сумму. Визуальный итог группы — точная сумма по валютам; он показывается +счёта и свою прогнозную сумму. Визуальный итог группы — точная сумма в RUB; он показывается только если полнота прогноза `complete` и у каждого события группы известна прогнозная сумма. Иначе итог скрыт, а строки остаются. Этот итог не равен итогам ответа API и не относится к полученному доходу. diff --git a/domains/instruments/bonds.md b/domains/instruments/bonds.md index 340a25d..5b299e8 100644 --- a/domains/instruments/bonds.md +++ b/domains/instruments/bonds.md @@ -31,9 +31,9 @@ завершённой дневной свечи до московской даты котировки; база ищется за 20 дней. Денежная оценка без НКД равна цене в процентах, умноженной на текущий номинал и делённой на 100. Она округляется до девяти десятичных знаков. Оценка с НКД -добавляет текущий накопленный купонный доход только при совпадении валют. +добавляет текущий накопленный купонный доход. Это ориентир без комиссии: котировка может быть старше текущего номинала и НКД. -FX-конвертация не выполняется, первоначальный номинал не заменяет текущий. +Первоначальный номинал не заменяет текущий. Условия выпуска: текущий и первоначальный номинал, погашение, оферта, количество купонов в год, лот. Отдельно отмечены плавающий купон, @@ -49,7 +49,7 @@ FX-конвертация не выполняется, первоначальн Текущая купонная доходность — ближайший купон × 365 / длительность его периода в днях / денежная цена без НКД × 100 %. Decimal-расчёт округляется до четырёх знаков. Требуются известная неотрицательная выплата, положительные цена и -период и одинаковая валюта выплаты и цены. Это годовая оценка при сохранении +период. Это годовая оценка при сохранении размера купона и номинала, без налогов, комиссий и реинвестирования; она не является доходностью к погашению. Время котировки показывается отдельно. @@ -79,7 +79,7 @@ HTML-источник, общий кэш и поведение при измен Позиция агрегирует только `BOND` с точным UID по доступным счетам, используя [снимок портфеля](../portfolio/overview.md) и decimal-арифметику. Средняя покупка взвешена -по количеству. Неполные или разновалютные суммы недоступны; общая средняя +по количеству. Неполные суммы недоступны; общая средняя и процент доступны только при положительных включённых позициях. Для коротких позиций и сочетания длинных с короткими эти показатели не синтезируются. Результат открытой позиции не включает выплаченные купоны и продажи и не diff --git a/domains/instruments/stocks.md b/domains/instruments/stocks.md index fd4cd54..9741a3c 100644 --- a/domains/instruments/stocks.md +++ b/domains/instruments/stocks.md @@ -30,7 +30,7 @@ Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: скринер запускается кнопкой «Найти» с необязательным названием или тикером, сектором, -типом акции, валютой расчётов, площадкой, страной риска и максимальным размером +типом акции, площадкой, страной риска и максимальным размером лота. Он использует полный полученный каталог и показывает только акции, для которых каталог подтверждает доступность покупки и торговли через API. Это не гарантирует, что конкретная заявка пользователя будет исполнена прямо сейчас. @@ -90,10 +90,10 @@ UID включён в ключ кэша и подпись общего курс восстанавливается переходом на первую страницу с повторным запросом. [Портфель](../portfolio/overview.md) остаётся владельцем снимков и оценки позиций. Сводка -фильтрует `STOCK` по точному UID и использует точную десятичную арифметику, без конвертации валют. +фильтрует `STOCK` по точному UID и использует точную десятичную арифметику, в RUB. Средняя цена равна сумме количеств, умноженных на средние цены, делённой на общее количество; она округляется сервером до девяти знаков, проценты — до шести. -Неполные или разновалютные денежные итоги недоступны. Общая средняя и процент доступны только если все включённые позиции +Неполные денежные итоги недоступны. Общая средняя и процент доступны только если все включённые позиции положительны; для нулевого количества, коротких позиций и сочетания длинных с короткими они не синтезируются. Результат открытой позиции исключает продажи и дивиденды и не называется полной diff --git a/domains/operations/overview.md b/domains/operations/overview.md index 88dd087..042bdf7 100644 --- a/domains/operations/overview.md +++ b/domains/operations/overview.md @@ -13,7 +13,7 @@ Исполненная операция содержит тип события, его категорию, описание, момент совершения, доступные сведения об инструменте и денежное движение. Запросы отменённых или ожидающих исполнения поручений не образуют этот журнал. -Суммы остаются точными десятичными строками в исходной валюте. Frontend +Суммы остаются точными десятичными строками в RUB. Frontend форматирует их и считает визуальный итог группы по показанным записям (см. [группировку](#группировка-в-режиме-все-счета)). Положительное и отрицательное движение сохраняют знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму. @@ -31,7 +31,7 @@ затем группирует только их. Полный журнал группирует страницу, уже загруженную курсором; группы не переносятся между страницами. Курсор и порядок записей не меняются. -Итог группы — точная сумма по каждой валюте, без конвертации. Он показывается только +Итог группы — точная сумма в RUB. Он показывается только если полнота журнала `complete`, у каждой записи группы есть `payment`, и ни одна сумма не равна нулю. Иначе итог скрыт, а строки счетов остаются. @@ -48,7 +48,7 @@ но их недоступность не меняет финансовое событие. Журнал не вычисляет остаток после операции, историческую стоимость портфеля, -совокупный налог или валютную конвертацию. Изменение рыночной цены не становится +совокупный налог. Изменение рыночной цены не становится операцией. Переводы между счетами не схлопываются в одну запись; одинаковые ID операций разных счетов не означают дубликат. diff --git a/domains/portfolio/all-accounts.md b/domains/portfolio/all-accounts.md index 6039389..b158699 100644 --- a/domains/portfolio/all-accounts.md +++ b/domains/portfolio/all-accounts.md @@ -30,12 +30,13 @@ Backend публикует статические маршруты под `/api/ У них те же формы данных и фильтры, что у соответствующих разделов отдельного счёта. Позиции, операции (включая выплаты дохода) и события сохраняют источник в поле `account: { id, name }`; в ответах одного счёта это поле отсутствует. -Валютные итоги и деньги агрегированы. Точную форму транспорта задаёт +Итоги и деньги агрегированы в RUB. Точную форму транспорта задаёт [OpenAPI backend](../../architecture/http-api.md). -- Портфель и доступные денежные остатки суммируются сервером точно, по исходной - валюте, с точной десятичной арифметикой. FX-конвертации нет; правила - неизвестных значений и состава сумм наследуются от [портфеля](data-reference.md). +- Портфель и доступные денежные остатки суммируются сервером точно, в RUB, + с точной десятичной арифметикой. Позиции и деньги в других валютах не + участвуют; правила неизвестных значений и состава сумм наследуются от + [портфеля](data-reference.md). - Одинаковые бумаги разных счетов показаны одной карточкой с общим названием и идентификатором. Внутри всегда видны строки счетов: ссылка на портфель, количество, доступная стоимость, результат, средняя цена и доля позиции. @@ -48,7 +49,7 @@ Backend публикует статические маршруты под `/api/ API сохраняет позиции каждого счёта; объединяется только их представление. Порядок карточек задаёт первое вхождение бумаги; порядок счетов внутри карточки сохраняется. Одиночные позиции - используют прежнюю карточку. Доля относится к позициям той же валюты + используют прежнюю карточку. Доля относится к позициям во всём показанном портфеле, без денежных остатков. - Журнал объединяет операции по убыванию времени, при равном времени использует порядок идентификаторов счетов и операций. ID diff --git a/domains/portfolio/concentration.md b/domains/portfolio/concentration.md index eed9b9c..d90e6eb 100644 --- a/domains/portfolio/concentration.md +++ b/domains/portfolio/concentration.md @@ -46,21 +46,10 @@ полный перечень остальных элементов, каждый со своей суммой и долей; топ-5 и диаграмма при этом не меняются. -Доли считаются от суммы стоимостей позиций разреза в выбранной валюте и +Доли считаются от суммы стоимостей позиций разреза и в сумме дают ровно 100 %; округление — до десятой доли процента, очень малые доли показываются как «< 0,1 %». Отрицательные и нулевые стоимости, а также -позиции без оценки в разрез не входят. Конвертации валют нет: каждая позиция -входит только в валюту своей оценки. - -## Валюта - -У вкладки свой список валют — валюты оценок позиций акций, облигаций и фондов, -отсортированные по ISO-коду, — и свой выбор, независимый от «Структуры -портфеля». По умолчанию выбрана первая валюта списка. При одной валюте -переключатель не показывается. Общего итога по нескольким валютам нет. -Выбранные разрез и валюта сохраняются, пока пользователь остаётся на странице -счёта, включая переключение её вкладок, и сбрасываются при новом заходе и при -смене счёта. +позиции без оценки в разрез не входят. ## Диаграмма и неполные данные @@ -69,7 +58,7 @@ отмечен как неполный (`completeness: "incomplete"` — часть позиций не имеет оценки), над разрезом появляется предупреждение «Данные неполные», а под списком — сноска «Не учтено позиций: N» с числом позиций, не вошедших в разрез. -Пустой разрез (нет позиций с положительной оценкой в выбранной валюте) +Пустой разрез (нет позиций с положительной оценкой) показывает текстовое состояние вместо диаграммы. ## Откуда берутся сектор и эмитент diff --git a/domains/portfolio/data-reference.md b/domains/portfolio/data-reference.md index 025046a..d7e2fdf 100644 --- a/domains/portfolio/data-reference.md +++ b/domains/portfolio/data-reference.md @@ -9,6 +9,7 @@ Деньги представлены значением Money: ISO-код валюты и десятичная строка, например `{ "currency": "RUB", "value": "12500.5" }` — условный пример. +Валюта всегда `RUB`: система работает только с рублями. Количества также передаются десятичными строками. Backend выполняет десятичную арифметику без промежуточного `Number`, нормализует отрицательные целую и дробную части и убирает незначащие конечные нули. Экспоненциальной @@ -18,7 +19,7 @@ `null` означает, что показатель нельзя определить из доступных данных. Это не ноль. Известная нулевая цена отличается от отсутствующей; позиция с ненулевым количеством и неизвестной стоимостью остаётся в составе. -Несколько валют никогда не складываются в одну общую стоимость. +Позиции и деньги в валютах, отличных от RUB, отфильтровываются при получении из T-Bank и не участвуют ни в одном итоге. ## Состав и денежные показатели @@ -26,25 +27,25 @@ | --- | --- | --- | | Позиция | Ненулевое количество обычного инструмента из `GetPortfolio.positions` | Виртуальные позиции не включаются; порядок источника сохраняется | | `quantity` | Количество из текущей позиции брокера | Допустимы дробные и отрицательные количества; это не число лотов | -| `currentValue` | Количество × текущая цена из `GetPortfolio` | `null`, если цена или её валюта недоступны | +| `currentValue` | Количество × текущая цена из `GetPortfolio` | `null`, если цена недоступна | | `averagePurchasePrice` | Средняя цена позиции, переданная брокером | Это цена единицы, а не сумма затрат или рассчитанная приложением история покупок | -| `absoluteReturn` | Количество × (текущая цена − средняя цена) | Требует обеих цен в одной известной валюте; не включает продажи, купоны и дивиденды | -| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией | -| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля | +| `absoluteReturn` | Количество × (текущая цена − средняя цена) | Требует обеих цен; не включает продажи, купоны и дивиденды | +| Cash Balance | Суммы RUB из `GetPositions.money`, объединённые в один остаток | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией | +| Итог Portfolio | Стоимости позиций + Cash Balance в RUB | Не включает невычислимую стоимость; не обещает полную оценку портфеля | | `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время | | `isStale` | Истёк часовой срок свежести сохранённого portfolio source | Не описывает состояние клиентского кэша или ошибку фонового запроса | | `sector` | Сектор инструмента в том виде, как его определяет T-Bank, с обрезанными пробелами | Только для позиций `STOCK`, `BOND` и `FUND`; значение из одних пробел считается отсутствующим. Во всех остальных случаях — `null` | | `issuer` | Бренд актива инструмента по данным T-Bank: `{ id, name }` | Только для позиций `STOCK`, `BOND` и `FUND`, когда у актива известны uid и название бренда. Во всех остальных случаях — `null`. Не влияет на количество, стоимость и свежесть снимка; используется вкладкой [«Концентрация»](concentration.md) | -Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых +Ненулевые денежные остатки суммируются в один Cash Balance. Позиции одинаковых инструментов в ответе отдельного счёта не объединяются; визуальная группировка между счетами описана [отдельно](all-accounts.md#данные-и-вычисления). Сводка уже включает Cash Balance: прибавление денег к ней второй раз неверно. При отсутствии вычислимой стоимости у части позиций известный итог остаётся полезен, но представляет только оценённую часть. Известные нулевые итоги -отдельного счёта не создают валютные карточки. Если `totalsByCurrency` пуст, -но позиции или деньги присутствуют, это не пустой портфель. +отдельного счёта не создают отдельную итоговую карточку. Если итог Portfolio +отсутствует, но позиции или деньги присутствуют, это не пустой портфель. ## Результат инвестирования @@ -81,33 +82,32 @@ ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя облигации (`BOND`), прочие инструменты (`OTHER`) и деньги (`CASH`). Торгуемые валютные инструменты входят в `OTHER`; денежные остатки — в `CASH`. Сервер строит структуру из позиций и денег, без повторного добавления -`totalsByCurrency`. +итога Portfolio. Доля категории — её стоимость / известная стоимость позиций и денег -в выбранной валюте × 100. Backend округляет процент до шести знаков; +× 100. Backend округляет процент до шести знаков; frontend показывает до сотых. Положительное значение меньше 0,01% отображается как «< 0,01 %», если серверное округление не превратило его в ноль. В геометрию диаграммы передаются только числовые проценты; суммы в списке и подсказке форматируются из исходных точных строк. Доля на карточке отдельной позиции использует другой знаменатель: -сумму стоимостей всех позиций той же валюты **без денег**. Это приблизительный +сумму стоимостей всех отображаемых позиций **без денег**. Это приблизительный показатель для отображения, рассчитанный frontend. Он не изменяет Money. В режиме всех счетов знаменатель охватывает все показанные счета. | Состояние | Доступное представление | | --- | --- | -| Все составляющие оценены, неотрицательны, итог положителен | Суммы, доли и кольцевая диаграмма выбранной валюты | -| Есть позиция без оценки | `unvaluedCategories` содержит её категорию и количество; суммы известной части остаются, доли и кольцо недоступны для всех валют | -| Есть отрицательная составляющая | Суммы со знаком остаются; доли и кольцо отключены для её валюты, даже если итог категории положителен | +| Все составляющие оценены, неотрицательны, итог положителен | Суммы, доли и кольцевая диаграмма | +| Есть позиция без оценки | `unvaluedCategories` содержит её категорию и количество; суммы известной части остаются, доли и кольцо недоступны | +| Есть отрицательная составляющая | Суммы со знаком остаются; доли и кольцо отключены, даже если итог категории положителен | | Состав пуст или имеет нулевую стоимость | Текстовое состояние вместо кольца | -Неоценённой позиции нельзя достоверно назначить валюту по её стоимости, -поэтому ограничение долей действует на все валюты. В общем портфеле проверка +В общем портфеле проверка отрицательных денег выполняется по остаткам счетов до взаимозачёта. Нормативные основы: [нормализованный состав](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-нормализованный-состав-portfolio) -и [точные значения и валютные агрегаты](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-точные-значения-и-валютные-агрегаты). +и [точные значения и итоговая сумма в RUB](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-точные-значения-и-итоговая-сумма-в-rub). Структура и общий режим подтверждены реализацией и её проверками; отдельных требований к ним в основной спецификации портфеля пока нет. Поля транспорта следует сверять с [HTTP API](../../architecture/http-api.md), diff --git a/domains/portfolio/how-to/investigate-discrepancy.md b/domains/portfolio/how-to/investigate-discrepancy.md index 5b5e446..326dfea 100644 --- a/domains/portfolio/how-to/investigate-discrepancy.md +++ b/domains/portfolio/how-to/investigate-discrepancy.md @@ -7,7 +7,7 @@ базы для разбора не нужны; не переносите их в документацию или отчёт. 1. **Определите предмет сравнения.** Запишите, открыт отдельный счёт или - «Все счета», какая валюта и какой показатель расходятся. Текущая стоимость, + «Все счета» и какой показатель расходится. Текущая стоимость, свободные деньги, результат открытой позиции и полученный доход — разные величины. Убедитесь, что сравниваемые значения относятся к одному составу счетов и инструменту. Название или тикер сами по себе не доказывают @@ -19,9 +19,9 @@ автоматически, поэтому расхождение проверяйте по показанному состоянию источника, а не по результату ручного обновления. Перезагрузка backend сохраняет последний полный снимок в SQLite, но не создаёт историю стоимости. -3. **Проверьте состав суммы.** Для одной валюты итог складывается из - вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги - второй раз, не складывайте валюты и не включайте заблокированные средства. +3. **Проверьте состав суммы.** Итог складывается из вычислимых стоимостей + позиций и доступных денег в RUB. Не добавляйте деньги второй раз и не + включайте заблокированные средства. Ненулевая позиция с `currentValue: null` делает оценку неполной, но не даёт нулевой вклад в смысле полной стоимости. `CURRENCY` участвует в суммах, хотя текущий интерфейс скрывает её карточку — diff --git a/domains/portfolio/investment-performance.md b/domains/portfolio/investment-performance.md index 13c12ed..cf8bcb3 100644 --- a/domains/portfolio/investment-performance.md +++ b/domains/portfolio/investment-performance.md @@ -44,7 +44,6 @@ | `HISTORY_START_UNKNOWN` | Неизвестна дата открытия счёта | | `OPERATIONS_HISTORY_INCOMPLETE` | История операций не получена полностью до момента оценки | | `PORTFOLIO_VALUATION_INCOMPLETE` | Нет полной оценки портфеля | -| `NON_RUB_ACTIVITY` | Есть потоки или остатки не в рублях | | `UNVALUED_SECURITIES_TRANSFER` | Бумаги введены или выведены без денежной оценки | | `UNCONFIRMED_TRANSFER` | В «Все счета» есть похожие на перевод между своими счетами операции, связь которых источник не подтверждает | | `ACCOUNT_VALUATION_DATES_DIFFER` | В «Все счета» последние оценки счетов сделаны в разные московские дни | diff --git a/domains/portfolio/overview.md b/domains/portfolio/overview.md index 16c678b..f54d849 100644 --- a/domains/portfolio/overview.md +++ b/domains/portfolio/overview.md @@ -10,9 +10,10 @@ ## Снимок и связанные области Снимок состоит из позиций по инструментам, доступных денежных остатков, -валютных итогов и структуры активов. Позиция означает ненулевое количество +итоговой суммы в RUB и структуры активов. Позиция означает ненулевое количество обычного инструмента в брокерском портфеле. Торгуемая валютная позиция -`CURRENCY` и свободные деньги имеют разный смысл и передаются раздельно. +`CURRENCY` и свободные деньги имеют разный смысл и передаются раздельно; +в итоге учитываются только деньги в RUB. Точный смысл сумм и неизвестных значений раскрыт в [справочнике данных](data-reference.md). @@ -34,18 +35,15 @@ Backend получает данные через серверную ## Как читать экран -Валютная сводка показывает отдельную карточку для каждого полученного итога. -Деньги уже включены в итог своей валюты. RUB, USD и EUR обозначены ₽, $ и €; -другие валюты сохраняют ISO-код. Вспомогательным технологиям доступны полное +Сводка показывает одну карточку с итоговой суммой в RUB. Деньги уже включены +в итог. Сумма обозначена ₽; вспомогательным технологиям доступны полное значение и название валюты. Суммы сохраняют все исходные цифры, знак и дробные позиции, включая `-0,5`; длинные значения переносятся без обрезания. -Карточки не интерактивны и занимают одну колонку на узком экране, -две или три на широком. Отсутствующая валюта не получает искусственную -нулевую карточку. Пустые итоги не скрывают доступный состав портфеля. +Карточка не интерактивна. Отсутствующая итоговая сумма не получает искусственный +ноль. Пустой итог не скрывает доступный состав портфеля. Структура после сводки распределяет известную стоимость по акциям, -облигациям, другим активам и свободным деньгам. При нескольких валютах -пользователь выбирает одну; при одной отдельного переключателя нет. +облигациям, другим активам и свободным деньгам в RUB. Кольцевая диаграмма дополнена текстовым списком сумм и долей и подсказкой; на узком экране список расположен под ней. Неизвестная оценка, отрицательные составляющие или нулевой итог ограничивают возможность показать доли — @@ -73,14 +71,14 @@ Backend получает данные через серверную Существует расхождение: [согласованная спецификация представления позиций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-представление-карточек-позиций-на-клиенте) требует доступности всех иных типов в отдельной секции, а текущие frontend и его проверка исключают `CURRENCY` только из списка карточек. Такие позиции -остаются в ответе, валютных итогах, структуре и знаменателе долей остальных -позиций. Это описание фактического ограничения; выбор между изменением -интерфейса и согласованием требований остаётся отдельной задачей. +остаются в ответе, но не входят в итоговую сумму, структуру; их учёт в знаменателе +долей остальных позиций требует проверки. Выбор между изменением интерфейса и +согласованием требований остаётся отдельной задачей. ## Свежесть и границы При первом запросе экран показывает каркас заголовка, сводки и разделов. -Три формы валютных карточек в каркасе не обещают наличие трёх валют. +Каркас итоговой карточки не обещает наличие суммы. Загрузка обозначена для вспомогательных технологий; декоративные элементы не содержат данных и не получают фокус. При обычном фоновом обновлении сохраняются предыдущие суммы и время успешного получения в UTC. @@ -95,10 +93,9 @@ Backend получает данные через серверную Правила времени и ошибок приведены в [потоке загрузки](flows/portfolio-loading.md). Отдельного чтения истории стоимости нет, но каждый полный снимок -сохраняет точку оценки по счёту и валюте для будущих поддерживаемых +сохраняет точку оценки по счёту в RUB для будущих поддерживаемых представлений; публичного графика в текущем решении нет. Нет также -валютной конвертации, учёта виртуальных позиций или заблокированных -средств. Последний полный снимок сохраняется после перезапуска; +учёта виртуальных позиций или заблокированных средств. Последний полный снимок сохраняется после перезапуска; обновлением личных данных владеет часовой обход, рассчитанный на один экземпляр backend. Действие «Обновить» выполняет только локальное перечитывание открытого экрана и не запускает работу с T-Bank. diff --git a/glossary.md b/glossary.md index e549a8e..2a1b624 100644 --- a/glossary.md +++ b/glossary.md @@ -91,7 +91,7 @@ _Не использовать_: тип аккаунта T-Bank, protobuf type. ## Portfolio Текущее нормализованное состояние инвестиций одного Brokerage Account, -включающее позиции, доступные денежные остатки и итоги по каждой валюте. +включающее позиции, доступный денежный остаток и итоговую сумму в RUB. _Не использовать_: общий портфель, баланс счёта. @@ -123,7 +123,7 @@ _Не использовать_: денежный остаток, cash balance. Вкладка страницы одного Brokerage Account, показывающая, какие акции, облигации, секторы и эмитенты занимают наибольшую часть текущей оценки -Portfolio, без валютной конвертации. Отличается от [структуры +Portfolio в RUB. Отличается от [структуры портфеля](domains/portfolio/data-reference.md#структура-и-доли): в её разрезах деньги не входят в знаменатель долей. @@ -188,8 +188,8 @@ _Не использовать_: аватар актива, иконка бум ## Cash Balance -Ненулевой доступный денежный остаток Portfolio в одной валюте; не является -Portfolio Position и передаётся отдельно от `positions` в `cashBalances`. +Ненулевой доступный денежный остаток Portfolio в RUB; не является +Portfolio Position и передаётся отдельно от `positions`. _Не использовать_: валютная позиция, заблокированные средства. @@ -202,7 +202,7 @@ _Не использовать_: история портфеля, snapshot дл ## Точка истории стоимости портфеля -Полная оценка одного Brokerage Account в одной валюте на момент успешного +Полная оценка одного Brokerage Account в RUB на момент успешного обновления Portfolio; последовательность таких точек описывает изменение оценки во времени, включая неторговые дни. Не является Portfolio Snapshot или [Результатом инвестирования](#результат-инвестирования); отсутствие цены @@ -293,8 +293,7 @@ _Не использовать_: активный refresh token, признак Прогнозируемое на ближайший период денежное или связанное корпоративное событие одного Brokerage Account, рассчитанное по текущей Portfolio Position; не является исполненной Ledger Entry. Прогнозная сумма события передаётся как -nullable Money: известные coupon, dividend и maturity содержат Money в исходной -валюте, offer или неизвестные денежные условия передаются как `null`. +nullable Money: известные coupon, dividend и maturity содержат Money в RUB, offer или неизвестные денежные условия передаются как `null`. В публичном API прогноз доступен через маршруты `/events`; в доменной документации событие называется Future Cash Event. @@ -310,12 +309,12 @@ _Не использовать_: upstream dividend type, MOEX row name. ## Realized Income Analytics Результат анализа исполненных купонов и дивидендов по счёту за календарный -период включительно в часовом поясе Europe/Moscow. Сохраняет исходные валюты -и знаки денежных выплат, переданных брокером. Это не налоговый расчёт, +период включительно в часовом поясе Europe/Moscow. Сохраняет знаки денежных выплат, +переданных брокером, в RUB. Это не налоговый расчёт, прогноз или [Результат инвестирования](#результат-инвестирования); правила расчёта объясняет [область дохода](domains/income/overview.md). -_Не использовать_: доходность выплат, общий доход в одной валюте, +_Не использовать_: доходность выплат, общий доход в нескольких валютах, гарантированный доход. ## Результат инвестирования diff --git a/integrations/tbank.md b/integrations/tbank.md index e5a1857..08977dc 100644 --- a/integrations/tbank.md +++ b/integrations/tbank.md @@ -11,7 +11,7 @@ WealthLedger обращается к Invest API на сервере по gRPC ov | Категория | Для чего используется | | --- | --- | | Открытые счета | Определить доступный состав счетов и проверить доступ к конкретному счёту | -| Портфель и денежные остатки | Показать текущий состав инвестиций и итоги по исходным валютам | +| Портфель и денежные остатки | Показать текущий состав инвестиций и итоговую сумму в RUB | | Исполненные операции | Построить журнал и выделить полученные купоны и дивиденды | | Инструменты и рынок | Имена, идентификаторы, цены, свечи, параметры акций и облигаций | | Классификация инструментов | Сектор акций, облигаций и фондов и бренд актива для разрезов [концентрации](../domains/portfolio/concentration.md) | diff --git a/overview.md b/overview.md index 19f7db6..3d16398 100644 --- a/overview.md +++ b/overview.md @@ -19,7 +19,7 @@ WealthLedger — локальное приложение для частного | --- | --- | | Найти свой инвестиционный счёт | [Список счетов](domains/accounts/overview.md), их типы и доступные сводки | | Вести банковские карты | [Ручной учёт](domains/cards/overview.md): независимые балансы, расходы, пополнения, переводы, возвраты и месячные сводки | -| Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоги по валютам | +| Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоговая сумма в RUB | | Увидеть, где сконцентрированы деньги | [Концентрация](domains/portfolio/concentration.md): крупнейшие бумаги, секторы и эмитенты счёта | | Разобрать уже совершённые движения | [Журнал операций](domains/operations/overview.md): исполненные операции с фильтрами и последовательной загрузкой истории | | Посчитать полученные выплаты | [Пассивный доход](domains/income/overview.md): исполненные купоны и дивиденды за календарный период | @@ -46,9 +46,9 @@ WealthLedger — локальное приложение для частного подтверждает фактическое зачисление. Полученные купоны и дивиденды сами по себе не равны доходности или результату после налогов. -Денежная сумма всегда относится к одной валюте. Недоступное значение -отличается от нуля, а объединение счетов не означает автоматическую -конвертацию всех валют. Краткие определения собраны в [глоссарии](glossary.md). +Система работает только с рублями (RUB): данные в других валютах отфильтровываются +при получении из T-Bank и MOEX и не участвуют в итогах. Недоступное значение +отличается от нуля, а объединение счетов не означает конвертацию. Краткие определения собраны в [глоссарии](glossary.md). ## Источники и границы продукта -- 2.54.0 From 0bb47edd03654370e1538a551994e60672240af4 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 13 Nov 2026 07:41:49 +0300 Subject: [PATCH 3/4] fix(deps): patch basic-ftp advisory (WealthLedger/main#56) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4aad7d..07a46bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1125,9 +1125,9 @@ } }, "node_modules/basic-ftp": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.3.tgz", - "integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.6.tgz", + "integrity": "sha512-0EjZCaodOz6yiokv2JA1VyKGNdGxULZ+GGlPgG9tm4d+T/bHtX1hUch8CumDneBfpIbiSdn7TaCGwpM+gwsOlw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 5779c29..20fec87 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "smol-toml": "1.9.1" }, "release-it": { - "basic-ftp": "6.2.3", + "basic-ftp": "6.2.6", "undici": "7.30.1" }, "markdown-it": "15.0.3" -- 2.54.0 From d05aae8c1158e915bc9c350418e4b641f8de3aa8 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 11 Nov 2026 11:44:34 +0300 Subject: [PATCH 4/4] chore(release): 0.1.9 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de76173..8f828a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.9](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.8...v0.1.9) (2026-11-11) + +### Documentation + +* describe RUB-only behavior (WealthLedger/main[#55](https://git.personal.ksv741.ru/WealthLedger/docs/issues/55)) ([#68](https://git.personal.ksv741.ru/WealthLedger/docs/issues/68)) ([7fbd427](https://git.personal.ksv741.ru/WealthLedger/docs/commit/7fbd4273baa82e06fa415d99d0eb034fcbd335c8)) +* investment performance vs Moscow Exchange index (WealthLedger/main[#54](https://git.personal.ksv741.ru/WealthLedger/docs/issues/54)) ([#67](https://git.personal.ksv741.ru/WealthLedger/docs/issues/67)) ([f504a0f](https://git.personal.ksv741.ru/WealthLedger/docs/commit/f504a0f5666a84dca4df13e70be35ef2e291a501)) + ## [0.1.8](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.7...v0.1.8) (2026-11-06) ### Maintenance diff --git a/package-lock.json b/package-lock.json index 07a46bd..7eb58ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-docs", - "version": "0.1.8", + "version": "0.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-docs", - "version": "0.1.8", + "version": "0.1.9", "devDependencies": { "@commitlint/cli": "21.2.4", "@commitlint/config-conventional": "21.2.4", diff --git a/package.json b/package.json index 20fec87..4948466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wealth-ledger-docs", - "version": "0.1.8", + "version": "0.1.9", "private": true, "engines": { "node": "24.22.1" -- 2.54.0