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) — +«как сработали вложенные деньги и обогнал ли я индекс Мосбиржи». Прогноз не +подтверждает фактическое зачисление. Полученные купоны и дивиденды сами по +себе не равны доходности или результату после налогов. Денежная сумма всегда относится к одной валюте. Недоступное значение отличается от нуля, а объединение счетов не означает автоматическую