docs: reorganize system knowledge by domain
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Брокерские счета
|
||||
|
||||
На странице `/accounts` пользователь выбирает открытый поддерживаемый
|
||||
инвестиционный счёт T-Bank и переходит к его портфелю. Для этого нужны
|
||||
действующая сессия и сохранённое
|
||||
[подключение T-Bank](../../integrations/tbank.md). Frontend обращается
|
||||
только к backend. Счёт не хранится как долговременное локальное владение
|
||||
независимо от брокерского подключения.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Пользователь
|
||||
participant F as Frontend
|
||||
participant B as Backend
|
||||
participant D as SQLite
|
||||
participant T as T-Bank gRPC
|
||||
U->>F: Открывает /accounts
|
||||
F->>B: GET /api/v1/brokerage-accounts (Bearer)
|
||||
B->>D: Читает подключение T-Bank
|
||||
alt Подключение отсутствует
|
||||
B-->>F: 409 TBANK_NOT_CONNECTED
|
||||
F-->>U: Переход к настройкам T-Bank
|
||||
else Подключение сохранено
|
||||
B->>T: GetAccounts(OPEN) over TLS
|
||||
T-->>B: Счета или ошибка
|
||||
par Для каждого поддерживаемого счета
|
||||
B->>T: GetPortfolio + GetPositions over TLS
|
||||
T-->>B: Позиции и деньги либо временная ошибка
|
||||
end
|
||||
B-->>F: Счета со сводками либо ошибка
|
||||
F-->>U: Карточки счетов или объяснение ошибки
|
||||
end
|
||||
```
|
||||
|
||||
## Счёт и его сводка
|
||||
|
||||
Брокерский счёт принадлежит системе брокера. WealthLedger получает его
|
||||
идентификатор, имя, тип и доступную дату открытия через текущее подключение;
|
||||
локальная сессия пользователя сама по себе не подтверждает доступ к счёту.
|
||||
Закрытый, неподдерживаемый или недоступный через подключение счёт не входит
|
||||
в текущий список. Порядок списка сохраняет порядок источника.
|
||||
|
||||
Поддерживаются брокерский счёт (`BROKERAGE`), ИИС (`INDIVIDUAL_INVESTMENT`),
|
||||
Инвесткопилка (`INVEST_BOX`) и фонд денежного рынка (`MONEY_MARKET_FUND`).
|
||||
Банковские и неизвестные типы исключаются. Имя очищается от внешних пробелов;
|
||||
пустое имя заменяется названием типа. Отсутствующая дата открытия остаётся
|
||||
неизвестной, а известная передаётся календарной датой без времени.
|
||||
|
||||
`GET /api/v1/brokerage-accounts` требует Bearer token и возвращает список
|
||||
с необязательной финансовой сводкой каждого счёта. Точная форма ответа
|
||||
принадлежит [HTTP API](../../architecture/http-api.md).
|
||||
|
||||
| Состояние `portfolioSummary` | Что оно означает |
|
||||
| --- | --- |
|
||||
| Доступные валютные итоги | Текущие стоимости позиций и деньги сложены точно в исходной валюте |
|
||||
| Доступная сводка с пустыми итогами | Портфель получен, но валютных итогов нет; это не ошибка загрузки |
|
||||
| `null` | Финансовая сводка этого счёта временно недоступна; метаданные и переход на счёт сохраняются |
|
||||
|
||||
Валюты не конвертируются и не объединяются в общую сумму. Деньги уже входят
|
||||
в валютный итог; они не образуют второй самостоятельный итог. Правила
|
||||
неизвестных стоимостей совпадают со [справочником портфеля](../portfolio/data-reference.md).
|
||||
|
||||
Сводка также сообщает число ненулевых обычных позиций акций и облигаций
|
||||
(`stockPositionCount`, `bondPositionCount`). Это количество наименований,
|
||||
а не сумма штук или лотов. Подсчёт использует уже полученные позиции,
|
||||
не требует метаданных и включает дробные и короткие позиции, даже если их
|
||||
стоимость неизвестна. Нулевые, виртуальные и иные типы позиций в эти два
|
||||
счётчика не входят. Пустой портфель даёт нулевые счётчики; недоступная
|
||||
сводка не подменяется нулями.
|
||||
|
||||
Временная недоступность сводки одного счёта остаётся локальной для него.
|
||||
Ошибка подключения, отклонение токена или невозможность получить исходный
|
||||
список не превращаются в частичный успешный список.
|
||||
|
||||
## Состояния
|
||||
|
||||
При первом запросе `/accounts` frontend показывает каркас заголовка
|
||||
и карточек; декоративные элементы не содержат данных и не получают
|
||||
клавиатурный фокус. После успешного ответа либо ошибки используются
|
||||
обычные нижеописанные состояния.
|
||||
|
||||
- `409 TBANK_NOT_CONNECTED`: список не запрашивается у T-Bank, экран ведёт в настройки.
|
||||
- `400 INVALID_TBANK_TOKEN`: сохранённый токен не удаляется, экран ведёт в настройки для замены или удаления.
|
||||
- `503 TBANK_UNAVAILABLE`: экран предлагает повторить загрузку или открыть настройки.
|
||||
- Неизвестная ошибка: экран предлагает только повторить загрузку.
|
||||
- Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов.
|
||||
|
||||
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. На широком экране карточки расположены в две колонки, на узком — в одну. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
|
||||
|
||||
## Свежесть и границы
|
||||
|
||||
Список запрашивается при открытии экрана без ручного обновления и без обновления при возврате фокуса. Frontend использует успешный кэш TanStack Query в течение 60 секунд, поэтому повторный вход в этот интервал не выполняет новый GET. Когда уже показанный список обновляется в фоне, он остаётся на экране: индикатор обновления и ошибка с повторной попыткой отображаются рядом с последним успешным ответом, включая пустой список. Это клиентский сохранённый ответ; серверный запасной список не выдаётся. Backend не хранит кэш и не передаёт метку свежести. Список не заменяет полный [портфель](../portfolio/overview.md),
|
||||
[журнал операций](../operations/overview.md) или историю закрытых счетов.
|
||||
Для совокупного результата доступен [режим «Все счета»](../portfolio/all-accounts.md).
|
||||
|
||||
Согласованное поведение описывает [спецификация счетов](../../../specs/openspec/specs/brokerage-accounts/spec.md).
|
||||
Её раздел об экране пока требует одноколоночного списка и не описывает
|
||||
счётчики акций и облигаций. Текущая реализация и её проверки используют
|
||||
две колонки на широком экране и показывают эти счётчики. Расхождение требует
|
||||
отдельного согласования требований; в этой документации отражён наблюдаемый экран.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Аутентификация и сессии
|
||||
|
||||
Аутентификация связывает локального [пользователя](../../glossary.md#user)
|
||||
с его сессией WealthLedger. Она открывает доступ к приложению; доступ
|
||||
к брокерским данным определяется отдельным
|
||||
[подключением T-Bank](../../integrations/tbank.md). Backend проверяет
|
||||
учётные данные и управляет сессиями, frontend восстанавливает состояние
|
||||
входа и выбирает доступные маршруты.
|
||||
|
||||
## Регистрация и вход
|
||||
|
||||
Пользователь регистрируется или входит по email и паролю. Регистрация сразу
|
||||
создаёт первую сессию. Внешние пробелы email удаляются, буквы приводятся
|
||||
к нижнему регистру. Backend требует пароль не короче 12 символов и не длиннее
|
||||
72 UTF-8 байтов.
|
||||
|
||||
**Известное расхождение:** сохранённое требование регистрации и проверка
|
||||
в форме frontend считают нижнюю границу в UTF-8 байтах, а backend — в символах.
|
||||
Не-ASCII пароль может пройти форму и получить отказ API. Это не принятое
|
||||
изменение password policy; требуется отдельное согласованное решение
|
||||
о проверке пароля и подтверждение полного HTTP-потока.
|
||||
|
||||
Access token действует 15 минут и хранится только в памяти браузера.
|
||||
Refresh token имеет серверный срок 7 дней, заменяется при восстановлении
|
||||
сессии и передаётся в cookie `refresh_token` с `httpOnly` и `SameSite=Lax`.
|
||||
Сроки определены в [спецификации сессий](../../../specs/openspec/specs/user-authentication/spec.md#requirement-access-и-refresh-сессии)
|
||||
и [ADR 006](../../adr/006-auth-session-policy.md).
|
||||
|
||||
После перезагрузки SPA получает новый access token через refresh-cookie.
|
||||
Пока результат неизвестен, пользователь видит нейтральный skeleton каркаса
|
||||
без преждевременного показа public или protected содержимого. После
|
||||
завершения восстановления приложение выбирает доступный маршрут.
|
||||
|
||||
Все маршруты, кроме `/login` и `/register`, требуют сессию. После входа
|
||||
пользователь возвращается на исходный внутренний маршрут, если он был.
|
||||
Logout доступен в настройках: frontend очищает локальную сессию независимо
|
||||
от состояния сети, а backend отзывает серверную сессию при предъявлении
|
||||
её действительного refresh-cookie.
|
||||
|
||||
## Восстановление сессии
|
||||
|
||||
Динамическая схема показывает восстановление после reload между SPA, auth API
|
||||
и локальным хранилищем сессий. SQLite здесь — файл базы backend.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as React SPA
|
||||
participant B as Backend API
|
||||
participant D as Локальная SQLite
|
||||
Note over F: Reload: access token в памяти отсутствует
|
||||
F->>B: Refresh с httpOnly cookie
|
||||
B->>D: Проверить Session и текущий hash token
|
||||
alt Действительная сессия, ротация выиграна
|
||||
B->>D: Атомарно заменить hash и срок
|
||||
B-->>F: Access token, профиль, новая refresh-cookie
|
||||
F->>B: Protected request с Bearer
|
||||
else Cookie нет или refresh отклонён
|
||||
B-->>F: 401 и очистка cookie
|
||||
Note over F: Готовое неавторизованное состояние
|
||||
end
|
||||
```
|
||||
|
||||
В одном экземпляре frontend одновременные refresh используют один
|
||||
незавершённый запрос. Между вкладками используется Web Locks, если браузер
|
||||
его поддерживает. Backend всё равно защищает ротацию атомарно: координация
|
||||
браузеров не заменяет серверную проверку.
|
||||
|
||||
Обычный защищённый запрос с `401 UNAUTHORIZED` проходит refresh и один повтор
|
||||
через общий транспорт; auth endpoints из этого цикла исключены. Само
|
||||
истечение access token не запускает фоновый таймер refresh.
|
||||
|
||||
## HTTP-граница
|
||||
|
||||
Endpoints, статусы и формы публичных ответов определяет backend OpenAPI;
|
||||
согласование производного клиента описано в
|
||||
[contract workflow](../../development/runtime-openapi-verification.md).
|
||||
Refresh-cookie ограничена auth API. Признак Secure зависит от режима
|
||||
окружения: локальный HTTP предполагает development mode, см.
|
||||
[локальное окружение](../../development/local-workspace.md).
|
||||
|
||||
HTTP-ошибка и транспортная ошибка refresh одинаково завершают начальное
|
||||
восстановление готовым неавторизованным состоянием. При refresh после
|
||||
защищённого `401` frontend очищает сессию в памяти и query cache, сохраняя
|
||||
исходный `401` для перехода ко входу. Общая форма ошибок и известное
|
||||
расхождение деталей валидации находятся в
|
||||
[HTTP API](../../architecture/http-api.md#error-contract).
|
||||
|
||||
## Ограничения
|
||||
|
||||
Описанный auth flow ограничен регистрацией, входом, восстановлением сессии
|
||||
и logout. Подтверждение email, восстановление или смена пароля, смена email,
|
||||
удаление аккаунта, список устройств, logout всех устройств и rate limit
|
||||
попыток входа не входят в эту реализованную возможность. Access token
|
||||
может оставаться действительным до 15 минут после logout.
|
||||
|
||||
Если несколько клиентов одновременно предъявляют один действительный
|
||||
refresh token, сервер завершает одну ротацию успешно, остальные запросы
|
||||
получают `401`. Сессия победившего запроса остаётся действительной. Уже
|
||||
заменённый refresh token также даёт `401`, но сам по себе не отзывает сессию:
|
||||
механизм не хранит историю token или grace-window, чтобы отличить
|
||||
конкурентный запрос от replay. Это ограничение и его причины сохранены
|
||||
в [ADR 009](../../adr/009-concurrent-refresh-rotation-policy.md).
|
||||
|
||||
Refresh-cookie не задаёт собственные `maxAge` или `expires`.
|
||||
Семидневный серверный срок refresh token не является обещанием сохранения
|
||||
входа после закрытия браузера.
|
||||
|
||||
## Ответственность и проверка
|
||||
|
||||
Backend проверяет учётные данные и tokens, хранит состояние Session и
|
||||
атомарно выполняет ротацию. Frontend управляет состоянием входа,
|
||||
восстановлением после reload и доступностью экранов; общий HTTP-транспорт
|
||||
передаёт Bearer token и выполняет описанный refresh/retry.
|
||||
|
||||
Проверки конкурентной ротации, браузерного состояния и транспорта охватывают
|
||||
разные границы. Их изолированная проверка не заменяет полный HTTP-поток
|
||||
и проверку атомарности с реальной изолированной базой.
|
||||
|
||||
Согласованное поведение находится в workspace-спецификации
|
||||
[User authentication](../../../specs/openspec/specs/user-authentication/spec.md).
|
||||
Правила начального ожидания — в
|
||||
[состояниях интерфейса](../../architecture/cross-cutting/ui-resilience.md).
|
||||
@@ -0,0 +1,127 @@
|
||||
# Пассивный доход брокерского счёта
|
||||
|
||||
Пассивный доход отвечает на вопрос «сколько брокер сообщил исполненными
|
||||
купонами и дивидендами за выбранный период». Пользователь открывает
|
||||
`/accounts/:accountId/income` из обзора счёта или
|
||||
[общий доход](../portfolio/all-accounts.md) по доступным счетам.
|
||||
Экран показывает итоги по валютам, сравнение и отдельную карточку каждой
|
||||
включённой выплаты. Источником служат [исполненные операции](../operations/overview.md),
|
||||
а не изменение цены портфеля и не календарь будущих выплат.
|
||||
|
||||
Суммы сохраняют знак брокерского денежного движения. Налоговые операции
|
||||
не включаются; результат не называется суммой до или после налогов,
|
||||
инвестиционной эффективностью или налоговым расчётом.
|
||||
|
||||
## Период и данные
|
||||
|
||||
`GET /api/v1/brokerage-accounts/:accountId/income-summary` возвращает текущий месяц по сегодняшнюю дату для компактного блока обзора. Он не зависит от периода, который пользователь ранее выбрал на странице аналитики.
|
||||
|
||||
`GET /api/v1/brokerage-accounts/:accountId/income-analytics?from=YYYY-MM-DD&to=YYYY-MM-DD` возвращает полный результат. Даты задают включительный период в `Europe/Moscow`; без них применяется текущий месяц. Период не может быть будущим или обратным. Максимальная длительность периода не ограничена.
|
||||
|
||||
Оба запроса требуют Bearer token и используют обычные ответы о неподключённом T-Bank, отклонённом токене, недоступном счёте и временной недоступности источника.
|
||||
|
||||
## Выбор периода и состояния страницы
|
||||
|
||||
В блоке «Период» два календарных поля Mantine DateInput показывают даты в формате
|
||||
`ДД.ММ.ГГГГ`. Календарный выбор и ручной ввод изменяют только черновик. Кнопка
|
||||
«Применить» или отправка формы с клавиатуры проверяют видимый текст и передают допустимые
|
||||
даты в URL как `from=YYYY-MM-DD&to=YYYY-MM-DD`. До применения результат сохраняется; отдельной
|
||||
подписи периода под фильтрами нет. Открытие URL и навигация назад восстанавливают
|
||||
применённый период.
|
||||
|
||||
Пустая, несуществующая или будущая дата получает ошибку у своего поля;
|
||||
обратный диапазон — у поля «По». Некорректный текст не заменяется последней
|
||||
допустимой датой при потере фокуса, не меняет URL и не отправляет запрос.
|
||||
Календарь учитывает допустимую соседнюю дату и сегодняшний день в Europe/Moscow.
|
||||
«Сбросить» сразу применяет текущий московский месяц по сегодняшний день,
|
||||
восстанавливает оба поля и очищает ошибки, даже если изменён только черновик.
|
||||
|
||||
При первой загрузке или новом периоде без сохранённых данных показан
|
||||
каркас страницы. Пустой результат объясняет отсутствие исполненных купонов и
|
||||
дивидендов, сохраняя фильтры, налоговое пояснение и сравнения. Ошибка запроса
|
||||
или нарушение принадлежности выплат даёт локальное состояние с действием «Повторить».
|
||||
При фоновом обновлении того же периода сохранённый ответ и черновик
|
||||
остаются видимыми: загрузку обозначает индикатор, неуспешное обновление —
|
||||
предупреждение с повторной попыткой.
|
||||
|
||||
На узком экране поля и действия идут вертикально; все карточки выплат
|
||||
остаются в общем потоке страницы без раскрытия или внутренних областей прокрутки.
|
||||
|
||||
## Какие выплаты входят в результат
|
||||
|
||||
- Backend проверяет доступ к счёту, проходит все страницы исполненных операций и не возвращает частичный результат при ошибке страницы или повреждённой цепочке курсоров.
|
||||
- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход.
|
||||
- Итоги, сравнения, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. На странице показано только сравнение с годом ранее, когда календарная дата и история счёта позволяют. Предыдущий период остаётся в ответе API, но не отображается.
|
||||
- Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется.
|
||||
- Раздел «Выплаты» показывает единый список карточек выплат в исходном порядке `entries`: одна карточка на каждую выплату. Внешних групп по инструментам или валютам, итогов и количества выплат инструмента нет. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой карточке; валюты по-прежнему разделены в итогах и сравнениях. Дополнительный запрос за составом не нужен.
|
||||
|
||||
Сравнение с непосредственно предыдущим периодом сохраняется в API:
|
||||
для полного месяца это предыдущий полный месяц, иначе — непосредственно
|
||||
предшествующий интервал той же включительной длительности. Текущая страница
|
||||
его не показывает. Это расходится с
|
||||
[основным требованием сравнений](../../../specs/openspec/specs/brokerage-account-income-analytics/spec.md#requirement-period-comparisons-preserve-calendar-and-currency-meaning),
|
||||
которое описывает оба сравнения. Требуется отдельно согласовать этот разрыв;
|
||||
здесь сохранено различие между ответом API и фактическим экраном.
|
||||
|
||||
Раздел «Годом ранее» показывает цветные панели без общей белой подложки и
|
||||
внутренних отступов у внешнего блока. Точные периоды показаны в заголовках двух панелей с русским форматом диапазона
|
||||
и UTC для календарных дат.
|
||||
Отдельного заголовка с датами и валютного бейджа нет; валюта остаётся у каждой суммы.
|
||||
Панель «Разница» сохраняет серверную абсолютную разницу и дополняет итог, купоны
|
||||
и дивиденды процентом: абсолютная разница / сумма годом ранее × 100.
|
||||
Это округлённый показатель для отображения, а не пересчёт Money или инвестиционная
|
||||
доходность. Форматирование сохраняет знак и не более одного десятичного разряда.
|
||||
При нулевой или отрицательной базе либо непредставимом числовом отношении
|
||||
процентный бейдж не отображается; абсолютная разница остаётся видимой.
|
||||
|
||||
## Полнота и принадлежность выплат
|
||||
|
||||
Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`,
|
||||
равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ
|
||||
один раз при подготовке записей дохода и использует его же для группировки.
|
||||
Приоритет UID, FIGI, ticker/type и ID операции с разделением по валюте остаётся
|
||||
серверным правилом; клиент воспринимает ключ как непрозрачную строку.
|
||||
Это поле принадлежит ответу аналитики; общий журнал операций его не требует.
|
||||
|
||||
Frontend проверяет принадлежность выплат по полученному ключу до подготовки
|
||||
плоского списка выплат. Отсутствующий, пустой или
|
||||
нестроковый ключ, неоднозначные ключи инструментов, неизвестная группа, другая
|
||||
валюта или несовпадение числа выплат приводят к существующему локальному
|
||||
состоянию ошибки аналитики с повторной попыткой. Частичный список выплат не показывается как
|
||||
полный; формула идентификации в клиенте не восстанавливается. Полностью пустой
|
||||
ответ допустим. Состав и исходный серверный порядок записей и знак сумм
|
||||
сохраняются, frontend не пересчитывает денежные итоги. Повторяющиеся ID
|
||||
операций остаются отдельными строками; повторные вхождения учитываются раздельно, а изменение источника логотипа
|
||||
позволяет заново показать изображение после буквенной заглушки.
|
||||
|
||||
## Совместимость ответа
|
||||
|
||||
При обновлении с версии без `instrumentKey` сначала обновляется backend:
|
||||
дополнительное поле совместимо с прежним
|
||||
frontend. Затем синхронизируются OpenAPI snapshot/generated types и обновляется
|
||||
клиент. При переключении клиент перезагружается, чтобы не переносить
|
||||
старые результаты в памяти и незавершённые запросы. Новый frontend требует новый
|
||||
backend; отсутствие ключа принадлежности не скрывается подстановкой прежней формулы.
|
||||
|
||||
При регрессии сначала возвращается прежний frontend вместе с соответствующим
|
||||
snapshot/types и перезагружается клиент; затем при необходимости возвращается
|
||||
backend. Старый frontend может временно работать с новым ответом с добавленным полем.
|
||||
Календарные фильтры и отдельные карточки выплат используют тот же контракт и не требуют
|
||||
дополнительного обновления backend. Схема SQLite, учётные данные и сообщённые брокером
|
||||
операции не меняются: откат
|
||||
не удаляет данные, производные результаты запрашиваются заново.
|
||||
|
||||
## Будущие события и ограничения
|
||||
|
||||
На странице дохода нет блоков «Динамика» и «Ближайший прогноз». Она не запрашивает
|
||||
`/events` и не зависит от его доступности. Самостоятельная страница
|
||||
[«Будущие события»](upcoming-events.md) по-прежнему доступна из обзора
|
||||
выбранного счёта и сохраняет прежнее поведение. Будущие выплаты
|
||||
не включаются в итоги полученного дохода или сравнения.
|
||||
|
||||
Аналитика только читает данные, не хранит историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог.
|
||||
|
||||
Основное проверяемое поведение — в [спецификации дохода](../../../specs/openspec/specs/brokerage-account-income-analytics/spec.md),
|
||||
точная форма ответа — в [HTTP API](../../architecture/http-api.md).
|
||||
Не следует использовать календарь будущих событий для восстановления
|
||||
состава уже полученного дохода.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Будущие события
|
||||
|
||||
Будущие события помогают увидеть известные выплаты и даты по текущим
|
||||
позициям счёта. Обзор `/accounts/:accountId` показывает до пяти ближайших
|
||||
событий, полный список находится на `/accounts/:accountId/events`.
|
||||
[Общий режим](../portfolio/all-accounts.md) сохраняет отдельные строки
|
||||
событий каждого доступного счёта. Ошибка или пустой результат превью
|
||||
не заменяет основной портфель.
|
||||
|
||||
Прогноз отличается от [полученного дохода](overview.md): он основан
|
||||
на текущем количестве бумаг и опубликованных условиях, не подтверждает
|
||||
будущее зачисление и не входит в итоги исполненных выплат.
|
||||
|
||||
## Смысл события
|
||||
|
||||
| Событие | Источник и дата | Денежная оценка |
|
||||
| --- | --- | --- |
|
||||
| Купон | Календарь облигации из MOEX `bondization` | Известная выплата на бумагу × текущая позиция |
|
||||
| Дивиденд | T-Bank `GetDividends`, дата реестра `record_date` | `dividend_net` на бумагу × текущая позиция |
|
||||
| Полное погашение | MOEX `bondization` | Известные условия погашения × текущая позиция |
|
||||
| Оферта | MOEX `bondization` | Сумма не синтезируется, `projectedAmount: null` |
|
||||
|
||||
Для дивиденда `dateKind: registry` явно означает дату закрытия реестра,
|
||||
а не гарантированную дату поступления денег. Отменённые выплаты `Cancelled`
|
||||
не включаются. Будущие частичные погашения и амортизация не синтезируются
|
||||
без надёжного расписания. Неизвестные или неприменимые денежные условия
|
||||
остаются `null`, а не заменяются нулём.
|
||||
|
||||
Суммы рассчитаны по количеству на `calculatedAt`. Они не учитывают будущие
|
||||
сделки, право владения на дату реестра и персональный налоговый расчёт.
|
||||
Есть неразрешённое ограничение интерпретации:
|
||||
[основная спецификация](../../../specs/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-forecasts-preserve-event-meaning-and-uncertainty)
|
||||
и текущая подсказка экрана называют прогноз суммой «до налогов», но для
|
||||
дивиденда реализация только умножает `dividend_net` на количество.
|
||||
Налоговый смысл этого поля не подтверждён локальными материалами; на
|
||||
[странице акции](../instruments/stocks.md#дивиденды-и-финансовые-показатели)
|
||||
он также не трактуется как персональная сумма до или после налога.
|
||||
Расчёт подтверждён, налоговую формулировку требуется согласовать отдельно.
|
||||
|
||||
## Период и список
|
||||
|
||||
Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней
|
||||
после начала, обе даты включены. Поля «С» и «По» позволяют изменить период:
|
||||
«Применить» сохраняет выбранные календарные даты в URL и запрашивает результат,
|
||||
«Сбросить» возвращает период по умолчанию. Обзор счёта сохраняет прогноз
|
||||
на текущую дату и следующие 30 дней.
|
||||
|
||||
События упорядочены по календарной дате. У каждой строки видны тип,
|
||||
инструмент, дата и доступная сумма в исходной валюте. Список не имеет
|
||||
пагинации и не конвертирует валюты. Пустой результат означает отсутствие
|
||||
известных поддерживаемых событий в выбранном окне, а не доказательство
|
||||
отсутствия всех будущих выплат. Инструмент без поддерживаемых данных
|
||||
не получает искусственную строку «нет данных».
|
||||
|
||||
API принимает необязательные `from` и `to` в формате `YYYY-MM-DD` для
|
||||
отдельного и общего счёта. Без `from` используется сегодня в UTC,
|
||||
без `to` — 30 дней после начала. Неверный формат или несуществующая дата
|
||||
дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`.
|
||||
Фильтрация на сервере предшествует сортировке и вычислению
|
||||
`incomeTotalsByCurrency`, включающего только купоны и дивиденды.
|
||||
Итоги остаются в ответе API; полный экран показывает список без общего итога.
|
||||
|
||||
Текущие controller, сервис и экран поддерживают выбор периода, тогда как
|
||||
[основная спецификация](../../../specs/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-account-page-presents-an-upcoming-events-list)
|
||||
описывает фиксированные 30 дней и экран без фильтров. Документация отражает
|
||||
реализованный выбор периода, не изменяя это несогласованное требование.
|
||||
|
||||
## Получение и отказы
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as Frontend
|
||||
participant B as Backend
|
||||
participant T as T-Bank
|
||||
participant M as MOEX ISS
|
||||
F->>B: Счёт и выбранный период событий
|
||||
B->>B: Проверяет сессию и текущее подключение
|
||||
B->>T: Получает открытые счета и позиции доступного счёта
|
||||
par События акций
|
||||
B->>T: GetDividends за выбранный период
|
||||
T-->>B: Известные дивиденды
|
||||
and События облигаций
|
||||
B->>M: Календарь bondization
|
||||
M-->>B: Купоны, погашения и оферты
|
||||
end
|
||||
B->>B: Применяет период и текущее количество бумаг
|
||||
B-->>F: Полный список известных событий либо ошибка
|
||||
```
|
||||
|
||||
Запрос требует действующей сессии и подключения T-Bank. Получение
|
||||
необходимых финансовых данных брокера или календаря MOEX завершается ошибкой
|
||||
`503`, если источник недоступен: частичный список не выдаётся как полный.
|
||||
Доступные метаданные инструментов дополняют название и логотип; когда
|
||||
исходного тикера достаточно для календаря, ошибка дополнительного поиска
|
||||
метаданных не скрывает события.
|
||||
|
||||
При первом открытии показан каркас заголовка и строк, без интерактивных
|
||||
заглушек. Пустой ответ сохраняет возможность изменить период.
|
||||
При обычном фоновом обновлении уже полученные данные остаются на экране,
|
||||
ошибка получает предупреждение и повторную попытку. Без выбранного счёта
|
||||
запрос не выполняется. Смена сессии и подключения следует
|
||||
[общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md).
|
||||
|
||||
Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md)
|
||||
и [MOEX ISS](../../integrations/moex.md),
|
||||
точные транспортные поля — [HTTP API](../../architecture/http-api.md).
|
||||
Календарь конкретного выпуска и личные исполненные операции доступны
|
||||
на страницах [акций](../instruments/stocks.md) и
|
||||
[облигаций](../instruments/bonds.md), где их смысл также разделён.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Облигации
|
||||
|
||||
Страница облигации объясняет условия конкретного выпуска, его рыночную цену,
|
||||
купонный календарь и личную позицию. Денежная оценка покупки, текущая купонная
|
||||
доходность, доходность источника к целевой дате и результат открытой позиции
|
||||
имеют разный смысл. Раздел сохраняет их отдельно, вместе с валютой,
|
||||
датой и ограничениями исходных данных.
|
||||
|
||||
## Поиск и идентификация выпуска
|
||||
|
||||
Раздел «Облигации» открывает `/bonds`: поиск по названию, тикеру или ISIN
|
||||
с двух символов. `/bonds/:identifier` принимает UID или точный тикер.
|
||||
Название и логотип облигации в портфеле счёта и «Всех счетах» ведут на её
|
||||
страницу по точному UID без предварительной загрузки карточек всех позиций.
|
||||
При открытии выпуска проверенный однозначный тикер заменяет UID в адресе,
|
||||
сохраняя период графика и фильтр событий.
|
||||
|
||||
Поиск выбирает один режим на нормализованный ISIN, сохраняя разные выпуски
|
||||
и записи без ISIN отдельно. Приоритет: доступность API, точный тикер, TQOB,
|
||||
затем UID. Ограничение — 30 результатов после выбора режима. Цена и ссылка
|
||||
принадлежат выбранному UID. Прямая ссылка из портфеля сохраняет исходный UID,
|
||||
если поиск предпочитает другой режим. Неоднозначный точный тикер возвращает
|
||||
`409 BOND_TICKER_AMBIGUOUS`; интерфейс предлагает выбрать выпуск через поиск.
|
||||
Поиск загружает параметры выпусков и котировки, но оставляет ссылки по UID:
|
||||
отдельная проверка тикера для каждого результата не выполняется.
|
||||
|
||||
## Содержание
|
||||
|
||||
Карточка показывает цену в процентах от номинала, время котировки и статус
|
||||
торгов. Изменение выражено в процентных пунктах относительно последней
|
||||
завершённой дневной свечи до московской даты котировки; база ищется за 20 дней.
|
||||
Денежная оценка без НКД равна цене в процентах, умноженной на текущий номинал
|
||||
и делённой на 100. Она округляется до девяти десятичных знаков. Оценка с НКД
|
||||
добавляет текущий накопленный купонный доход только при совпадении валют.
|
||||
Это ориентир без комиссии: котировка может быть старше текущего номинала и НКД.
|
||||
FX-конвертация не выполняется, первоначальный номинал не заменяет текущий.
|
||||
|
||||
Условия выпуска: текущий и первоначальный номинал, погашение, оферта,
|
||||
количество купонов в год, лот. Отдельно отмечены плавающий купон,
|
||||
амортизация, бессрочность и субординация. Неизвестные даты и суммы показываются
|
||||
как недоступные.
|
||||
|
||||
Сводка «Купон, рейтинг и доходность» показывает ближайший купон на одну
|
||||
облигацию, дату выплаты и тип из календаря Т-Банка. Неизвестная сумма ближайшей
|
||||
выплаты не заменяется суммой более позднего купона. Флаг плавающего купона
|
||||
используется только как запасной признак типа; отсутствие флага не означает
|
||||
фиксированный купон.
|
||||
|
||||
Текущая купонная доходность — ближайший купон × 365 / длительность его периода
|
||||
в днях / денежная цена без НКД × 100 %. Decimal-расчёт округляется до четырёх
|
||||
знаков. Требуются известная неотрицательная выплата, положительные цена и
|
||||
период и одинаковая валюта выплаты и цены. Это годовая оценка при сохранении
|
||||
размера купона и номинала, без налогов, комиссий и реинвестирования; она не
|
||||
является доходностью к погашению. Время котировки показывается отдельно.
|
||||
|
||||
Эффективная доходность берётся из `marketdata_yields.EFFECTIVEYIELD` [MOEX ISS](../../integrations/moex.md),
|
||||
с проверкой точных SECID, ISIN и BOARDID. Показываются собственная цена
|
||||
расчёта, дата торгов и целевая дата (погашение, оферта или другая дата источника).
|
||||
Котировка MOEX может отличаться от котировки Т-Банка и поступать с задержкой.
|
||||
Доходность не рассчитывается из неполного календаря будущих выплат.
|
||||
|
||||
Кредитный рейтинг выпуска читается по точному ISIN из публичного
|
||||
[реестра НКР](../../integrations/nkr.md). Отображаются агентство,
|
||||
дата рейтингового действия, время получения реестра и ссылка на выпуск.
|
||||
Отзыв показывается явно; уровень риска Т-Банка и рейтинг эмитента не
|
||||
подставляются вместо рейтинга выпуска. Отсутствие записи в НКР не означает
|
||||
отсутствия рейтингов других агентств. Причины выбора источника и пределы
|
||||
покрытия сохранены в [ADR 013](../../adr/013-nkr-bond-credit-ratings.md).
|
||||
|
||||
HTML-источник, общий кэш и поведение при изменении реестра описаны в
|
||||
[интеграции НКР](../../integrations/nkr.md#загрузка-кэш-и-отказ-источника).
|
||||
|
||||
График и таблица дневных цен сохраняют проценты от номинала и признак
|
||||
завершённости свечи. Исторические котировки не пересчитываются в деньги
|
||||
по сегодняшнему номиналу. Денежная средняя покупки не наносится на процентную
|
||||
шкалу. Периоды: месяц, полгода, год и вся известная история. Окна upstream
|
||||
не превышают 365 дней, одинаковые границы дедуплицируются. Если начало истории
|
||||
неизвестно, запрос «вся история» возвращает `BOND_HISTORY_START_UNAVAILABLE`.
|
||||
|
||||
Позиция агрегирует только `BOND` с точным UID по доступным счетам, используя
|
||||
[снимок портфеля](../portfolio/overview.md) и decimal-арифметику. Средняя покупка взвешена
|
||||
по количеству. Неполные или разновалютные суммы недоступны; общая средняя
|
||||
и процент доступны только при положительных включённых позициях. Для
|
||||
коротких позиций и сочетания длинных с короткими эти показатели не синтезируются.
|
||||
Результат открытой позиции не включает выплаченные купоны и продажи и не
|
||||
называется доходностью к погашению. При отсутствии позиции блок скрыт.
|
||||
|
||||
## Купоны и личные события
|
||||
|
||||
Календарь охватывает пять прошлых лет и следующий год. Будущие купоны
|
||||
сортируются по дате выплаты, прошедшие и недатированные доступны отдельно.
|
||||
Показаны сумма на облигацию, номер, тип, длительность периода и дата реестра,
|
||||
если они известны. Неопределённый будущий купон не заменяется нулём.
|
||||
|
||||
Личная оценка доступна только при положительных позициях, известной сумме,
|
||||
будущей выплате и известном ещё не прошедшем реестре. Она не подтверждает право
|
||||
на выплату и не учитывает налоги и будущие сделки. Время позиции и устаревание
|
||||
снимка показываются отдельно от календаря выпуска.
|
||||
|
||||
Личная история использует [журнал операций](../operations/overview.md) всех счетов с точным
|
||||
`instrumentUid`, включая прошлые годы. Фильтры: все, сделки, купоны, прочее.
|
||||
Смена фильтра сбрасывает пагинацию; истёкший курсор восстанавливается запросом
|
||||
первой страницы. Выплаты выпуска не подменяют личные зачисления.
|
||||
|
||||
## Получение данных и восстановление
|
||||
|
||||
Страницу облигации обслуживает инвестиционная подсистема backend.
|
||||
Запросы `/api/v1/bonds` требуют действующей сессии с Bearer token и текущего
|
||||
подключения T-Bank. Backend разрешает идентификатор и получает условия
|
||||
выпуска, котировку, свечи, позицию, купоны и показатели. Точную форму
|
||||
транспортного контракта задаёт [HTTP API](../../architecture/http-api.md).
|
||||
|
||||
`BondBy`, `FindInstrument` и `GetBondCoupons` используют способ
|
||||
декодирования, сохраняющий отсутствие полей. Рыночные RPC проходят
|
||||
общие TLS, очередь, ограничение частоты, deadline и повторные попытки. Кэш успешных и совместных
|
||||
незавершённых запросов ограничен LRU и сроком 30 секунд по пользователю,
|
||||
поколению подключения и параметрам. Ошибки не кэшируются; ошибки авторизации
|
||||
не подавляются при дополнительной загрузке данных поиска.
|
||||
Исходный ответ `BondBy` совместно используется поиском, карточкой, котировкой,
|
||||
графиком, купонами и сводкой показателей в пределах того же пользователя, поколения и TTL.
|
||||
Одновременная загрузка секций вызывает один `BondBy` на UID. Календарь
|
||||
и сводка совместно используют один запрос купонов; котировка переиспользуется
|
||||
при расчёте купонной доходности. MOEX и НКР загружаются только для открытого
|
||||
выпуска, независимо от поиска. Для 30 новых
|
||||
результатов поиск выполняет 32 RPC до повторных попыток: один `FindInstrument`,
|
||||
30 `BondBy` и один пакетный запрос цен. Общий ограничитель backend не заменяет
|
||||
квоты Т-Банка; при их исчерпании сохраняется `TBANK_UNAVAILABLE`.
|
||||
|
||||
Разделы загружаются независимо, показывают каркас загрузки и локальную повторную попытку.
|
||||
Ошибка фонового обновления сохраняет последние данные. Смена подключения
|
||||
сбрасывает поддерево клиентских запросов облигаций вместе с остальными финансовыми запросами;
|
||||
выход очищает кэш сессии. Общие график, позиция и личные события
|
||||
имеют общее представление на страницах акций и облигаций.
|
||||
|
||||
Основные спецификации [портфеля](../../../specs/openspec/specs/portfolio-view/spec.md)
|
||||
и [журнала](../../../specs/openspec/specs/brokerage-account-ledger/spec.md)
|
||||
описывают исходные финансовые понятия. Отдельная основная спецификация
|
||||
страницы облигации в текущем OpenSpec Store не найдена; условия вычислений
|
||||
подтверждены [сервисом облигаций](../../../backend/src/tbank/bond.service.ts)
|
||||
и его [проверками](../../../backend/src/tbank/bond.service.spec.ts).
|
||||
Границы источников описаны в [разделе интеграций](../../integrations/README.md),
|
||||
сходное разделение рыночных и личных данных — в [акциях](stocks.md).
|
||||
@@ -0,0 +1,172 @@
|
||||
# Акции
|
||||
|
||||
Страница акции связывает три разных вида сведений: публичные данные
|
||||
конкретного выпуска и компании, текущую личную позицию и исполненную
|
||||
историю операций. Публичная цена и календарь дивидендов доступны даже без
|
||||
открытой позиции; личная часть относится только к счетам текущего
|
||||
подключения T-Bank. Эти сведения не заменяют друг друга и имеют разное время
|
||||
наблюдения.
|
||||
|
||||
## Поиск и точная бумага
|
||||
|
||||
Авторизованный пользователь открывает `/stocks/SBERP` по тикеру. Старые
|
||||
адреса `/stocks/:uid` продолжают работать. Сервер возвращает `slug`: проверенный
|
||||
однозначный тикер либо исходный UID. При открытии UUID-ссылки страница заменяет
|
||||
адрес на тикер только при совпадении UID, сохраняя период и фильтр событий.
|
||||
Прямое открытие и обновление страницы с тикером сначала разрешает его в UID;
|
||||
цены, позиция и личные операции затем запрашиваются только по этому UID.
|
||||
Ссылки находятся в названиях и логотипах акций отдельного портфеля и обзора
|
||||
«Все счета». Названия сохраняют обычный цвет текста; буквенная заглушка
|
||||
логотипа также ведёт на страницу акции. Карточки получают проверенный `slug`
|
||||
через запрос акции по UID: при подтверждённом тикере обе ссылки используют
|
||||
его, иначе сохраняют UID. Одинаковые UID в разных счетах используют один запрос и общий
|
||||
кэш страницы акции. До загрузки и при ошибке остаётся рабочая ссылка по UID;
|
||||
загрузка адресов не блокирует портфель.
|
||||
Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: поиск
|
||||
по названию или тикеру через каталог Т-Банка, начиная с двух символов.
|
||||
Результаты разных выпусков, включая обыкновенные и привилегированные акции,
|
||||
имеют отдельные UID. Карточка результата показывает логотип, последнюю цену
|
||||
с её временем, тип бумаги, площадку, код режима, ISIN и доступность торгов через
|
||||
API. Поиск показывает одну карточку на нормализованный ISIN. До ограничения
|
||||
в 30 результатов выбирается представитель выпуска: доступность через API,
|
||||
точное совпадение тикера, затем режим TQBR; равные варианты упорядочены по UID.
|
||||
Записи без ISIN сохраняются отдельно по UID. Обыкновенные и привилегированные
|
||||
акции с разными ISIN не объединяются. Цена и ссылка принадлежат выбранному
|
||||
UID; данные разных режимов не смешиваются. Прямые ссылки из портфеля сохраняют
|
||||
исходный UID, даже если поиск выбрал другой режим этого выпуска; такая страница
|
||||
сохраняет UUID в адресе. Если один точный тикер относится к разным выпускам,
|
||||
сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`, а страница предлагает поиск.
|
||||
Поиск тикера нечувствителен к регистру, но использует точное совпадение:
|
||||
SBER не заменяется на SBERP. Однобуквенные тикеры также поддерживаются.
|
||||
Прямая ссылка не требует предварительного открытия портфеля. Хлебные крошки
|
||||
«Акции → название» возвращают к поиску.
|
||||
|
||||
## Содержание и состояния
|
||||
|
||||
Верхний блок объединяет название, тип акции, текущую цену и показатели компании.
|
||||
Денежные показатели используют символ валюты; описание компании видно сразу
|
||||
под логотипом и названием, перед ценой. Далее идут график, моя позиция, предстоящие
|
||||
дивиденды и личная история событий.
|
||||
Логотип загружается из публичного CDN; при отсутствии изображения показывается буквенная заглушка.
|
||||
Позиция отсутствующей или полностью проданной бумаги не создаёт пустого блока;
|
||||
рыночные данные и личная история остаются доступны.
|
||||
|
||||
Каждый раздел загружается независимо. Первичная загрузка показывает каркас
|
||||
раздела, ошибка — локальную повторную попытку. При ошибке фонового обновления
|
||||
сохраняются предыдущие данные и предупреждение. Цена содержит собственное время
|
||||
UTC, которое может быть старше времени получения ответа. Статус торгов не
|
||||
выводится из свежести цены. Отсутствующее значение не заменяется нулём.
|
||||
|
||||
## Связь с портфелем и операциями
|
||||
|
||||
Серверная инвестиционная подсистема предоставляет поиск, карточку инструмента,
|
||||
котировку, дневные свечи, сводку позиции, дивиденды и доступные данные компании.
|
||||
Все запросы `/api/v1/stocks` требуют действующей сессии с Bearer token и актуального подключения
|
||||
T-Bank; точные маршруты и поля принадлежат [HTTP API](../../architecture/http-api.md).
|
||||
Frontend не получает брокерский токен и не обращается к T-Bank напрямую.
|
||||
|
||||
Личная история использует существующий
|
||||
`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`.
|
||||
Фильтр UID валидируется и передаётся upstream как `instrumentId`; ответ также
|
||||
проверяется по UID. Без `from` такой запрос начинается с самой ранней известной
|
||||
даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch.
|
||||
Обычный журнал без фильтра инструмента сохраняет начало текущего года.
|
||||
UID включён в ключ кэша и подпись общего курсора: курсор другой бумаги,
|
||||
пользователя, поколения подключения или фильтра отклоняется. Истечение курсора
|
||||
восстанавливается переходом на первую страницу с повторным запросом.
|
||||
|
||||
[Портфель](../portfolio/overview.md) остаётся владельцем снимков и оценки позиций. Сводка
|
||||
фильтрует `STOCK` по точному UID и использует точную десятичную арифметику, без конвертации валют.
|
||||
Средняя цена равна сумме количеств, умноженных на средние цены, делённой на
|
||||
общее количество; она округляется сервером до девяти знаков, проценты — до шести.
|
||||
Неполные или разновалютные денежные итоги недоступны. Общая средняя и процент доступны только если все включённые позиции
|
||||
положительны; для нулевого количества, коротких позиций и сочетания
|
||||
длинных с короткими они не синтезируются.
|
||||
Результат открытой позиции исключает продажи и дивиденды и не называется полной
|
||||
доходностью. Время и `isStale` сохраняют семантику исходного Portfolio Snapshot.
|
||||
|
||||
## Рыночная цена и график
|
||||
|
||||
Изменение рассчитывается относительно последней завершённой дневной свечи
|
||||
строго до календарной даты последней цены по Москве. Поиск базы охватывает
|
||||
20 дней до последней цены; если подходящей свечи нет, сравнение недоступно.
|
||||
Дата базы указана рядом с изменением. `GetClosePrices` не используется:
|
||||
его основное закрытие может относиться к текущему дню.
|
||||
|
||||
График показывает дневные `close`, сохраняет `isComplete` и предупреждает
|
||||
о формирующейся свече. Окна RPC не превышают 365 дней, общие границы
|
||||
дедуплицируются. «Вся история» начинается с `first_1dayCandleDate`; если источник
|
||||
не сообщает начало истории, этот период недоступен, остальные работают.
|
||||
Пустое окно до начала торгов не считается ошибкой. Линия текущей средней
|
||||
покупки доступна только в валюте графика; шкала включает её значение.
|
||||
Таблица цен раскрывается под графиком: новые даты первыми, валюта в цене,
|
||||
статус завершённости свечи, закреплённая шапка и прокрутка с клавиатуры.
|
||||
Стили графиков подключены на уровне приложения и каталога, поэтому оформление
|
||||
подсказки не зависит от ранее посещённых страниц.
|
||||
|
||||
Метки старых сделок и дивидендов на графике не добавлены: соответствие шкалы
|
||||
после корпоративных изменений не подтверждено. Лента использует дату операции;
|
||||
количество сделки берётся из `quantityDone`, а не порученного `quantity`.
|
||||
Отсутствующее исполненное количество не подменяется порученным. Сами исполнения
|
||||
не превращаются в точные точки графика.
|
||||
|
||||
## Дивиденды и финансовые показатели
|
||||
|
||||
События компании отделены от личных зачислений. Известные будущие выплаты
|
||||
определяются по `paymentDate`, при её отсутствии — по `recordDate`; прошедшие
|
||||
и недатированные события помещены в отдельную историю компании. Отменённые
|
||||
события исключаются. Личная оценка умножает сумму на акцию на текущее положительное
|
||||
количество. Для уже прошедшего реестра оценка не строится. Время позиции и
|
||||
предупреждение об устаревшем снимке или недоступной оценке показаны отдельно.
|
||||
|
||||
Оценка не доказывает право на выплату, не учитывает будущие сделки и не обещает
|
||||
индивидуальную дату зачисления. Статусы «рекомендован/утверждён», сумма отдельного
|
||||
зачисления после налога и налоговый смысл `dividend_net` не выводятся из API.
|
||||
Пустой список означает отсутствие известных событий только за указанный период.
|
||||
|
||||
`GetAssetFundamentals` использует отдельный способ декодирования ответа,
|
||||
который сохраняет отсутствие полей (`defaults: false`). Это важно для
|
||||
отличия неизвестного показателя от известного нуля; настройка остальных
|
||||
клиентов при этом не изменяется. Финансовый сектор
|
||||
показывает капитализацию, P/E, P/B, ROE и чистую прибыль; остальные — капитализацию,
|
||||
P/E, выручку, EBITDA, чистую прибыль, общий долг и чистый долг / EBITDA, если
|
||||
значения доступны. Денежные показатели без валюты не показываются.
|
||||
TTM означает последние 12 месяцев, MRQ — последний отчётный квартал;
|
||||
время получения не выдаётся за дату отчётности. Годовых финансовых рядов нет.
|
||||
|
||||
## Получение данных и ограничения
|
||||
|
||||
Запросы T-Bank проходят общую серверную интеграцию с TLS, лимитом частоты,
|
||||
очередью, предельным временем ожидания и повторными попытками. Проверка доступа
|
||||
и расшифровка token выполняются только backend. Успешные ответы и совместные незавершённые запросы кэшируются
|
||||
в ограниченном кэше LRU на 30 секунд по пользователю, поколению подключения и параметрам.
|
||||
Для поиска `FindInstrument` дополняется сведениями `ShareBy` через тот же
|
||||
кэш карточки инструмента и одним пакетным `GetLastPrices` для списка. Ошибка
|
||||
дополнительной загрузки или исчезнувшая карточка не скрывает исходную запись
|
||||
каталога; недоступные логотипы и цены имеют заглушки. Ошибки авторизации
|
||||
не подавляются.
|
||||
Ошибка целого запроса не кэшируется; отсутствие дополнительного источника
|
||||
обозначается в ответе. Поддерево клиентских запросов акций сбрасывается вместе с финансовыми
|
||||
данными при изменении подключения; выход очищает кэш сессии.
|
||||
|
||||
Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и
|
||||
временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции,
|
||||
дополненный количеством, ценой и ссылкой на счёт. История
|
||||
дивидендов компании раскрывается в оформленном блоке с отдельными карточками
|
||||
выплат, датой реестра, доступной датой выплаты и суммой на акцию.
|
||||
Ограничения источника требуют различать отсутствие финансового поля и ноль.
|
||||
Время получения не подтверждает дату отчётности, а совпадение даты не позволяет
|
||||
однозначно связать налог с отдельным дивидендом.
|
||||
Алгоритм корректировки исторических цен после корпоративных событий не
|
||||
подтверждён, поэтому точное наложение старых личных сделок на график не обещается.
|
||||
Доступность данных по отдельным бумагам не доказывает полноту покрытия рынка.
|
||||
История открытых счетов не обещает полную историю владения, включая закрытые
|
||||
счета, переводы и универсальные корпоративные события.
|
||||
|
||||
Основные спецификации [портфеля](../../../specs/openspec/specs/portfolio-view/spec.md)
|
||||
и [журнала](../../../specs/openspec/specs/brokerage-account-ledger/spec.md)
|
||||
задают исходные финансовые понятия. Отдельная основная спецификация страницы
|
||||
акции в текущем OpenSpec Store не найдена; описанные механизмы проверяются
|
||||
по [сервису акций](../../../backend/src/tbank/stock.service.ts) и его
|
||||
[проверкам](../../../backend/src/tbank/stock.service.spec.ts).
|
||||
Сопоставление с облигациями — на [странице облигаций](bonds.md).
|
||||
@@ -0,0 +1,117 @@
|
||||
# Журнал операций
|
||||
|
||||
Журнал отвечает на вопрос «какие события брокер исполнил по счёту».
|
||||
Каждая запись соответствует одной операции T-Bank, а не вычисленному
|
||||
приложением изменению стоимости портфеля. Пользователь открывает полный
|
||||
журнал на `/accounts/:accountId/operations`; обзор счёта показывает пять
|
||||
последних записей и ссылку на него. Ошибка этого блока не скрывает портфель.
|
||||
[Общий режим](../portfolio/all-accounts.md#общая-пагинация-операций)
|
||||
объединяет операции доступных счетов, сохраняя источник каждой записи.
|
||||
|
||||
## Что означает запись
|
||||
|
||||
Исполненная операция содержит тип события, его категорию, описание,
|
||||
момент совершения, доступные сведения об инструменте и денежное движение.
|
||||
Запросы отменённых или ожидающих исполнения поручений не образуют этот журнал.
|
||||
Суммы остаются точными десятичными строками в исходной валюте; frontend
|
||||
только форматирует их. Положительное и отрицательное движение сохраняют
|
||||
знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму.
|
||||
|
||||
Категории объединяют разные события для фильтрации: сделка (`trade`),
|
||||
доход (`income`), налог (`tax`), комиссия (`fee`), перевод (`transfer`)
|
||||
и прочее (`other`). Принадлежность к категории дохода сама по себе не
|
||||
означает включение в [пассивный доход](../income/overview.md): тот использует
|
||||
только точные типы исполненных купонов и дивидендов.
|
||||
|
||||
Количество сделки берётся из исполненного `quantityDone`; если его нет,
|
||||
количество неизвестно и не заменяется порученным. Цена, НКД и комиссия
|
||||
сохраняются как доступные детали операции. Комиссия сделки не создаёт
|
||||
дубликат отдельной записи журнала. Метаданные логотипа дополняют представление,
|
||||
но их недоступность не меняет финансовое событие.
|
||||
|
||||
Журнал не вычисляет остаток после операции, историческую стоимость портфеля,
|
||||
совокупный налог или валютную конвертацию. Изменение рыночной цены не становится
|
||||
операцией. Переводы между счетами не схлопываются в одну запись; одинаковые
|
||||
ID операций разных счетов не означают дубликат.
|
||||
|
||||
## Период и навигация
|
||||
|
||||
Полный журнал позволяет выбрать период включительно и несколько категорий.
|
||||
Без периода начало — 1 января текущего года в UTC, конец — текущее время.
|
||||
Поля «С» и «По» используют формат `ДД.ММ.ГГГГ`, русский календарь и взаимные
|
||||
ограничения дат. Изменение полей меняет только черновик. «Применить» сохраняет
|
||||
его в URL, загружает результат и сбрасывает пагинацию; «Сбросить» очищает
|
||||
фильтры и сразу возвращает период по умолчанию и все категории.
|
||||
«Назад» и «Далее» используют последний применённый фильтр.
|
||||
|
||||
На [страницах акций](../instruments/stocks.md) и
|
||||
[облигаций](../instruments/bonds.md) этот же журнал становится личной историей
|
||||
конкретной бумаги. Фильтр `instrumentUid` передаётся брокеру и проверяется
|
||||
в ответе. Без явного начала периода он охватывает историю с открытия счёта;
|
||||
в общем режиме — с самой ранней известной даты открытия доступных счетов.
|
||||
Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет
|
||||
видеть прошлые операции уже проданной бумаги, но не обещает историю
|
||||
закрытых и недоступных счетов.
|
||||
|
||||
## Получение и полнота
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as Frontend
|
||||
participant B as Backend
|
||||
participant T as T-Bank
|
||||
F->>B: Запрашивает применённый период и страницу операций
|
||||
B->>B: Проверяет сессию и текущее подключение
|
||||
B->>T: Получает открытые счета
|
||||
T-->>B: Подтверждает доступность счёта
|
||||
alt Подходящая страница есть в кэше
|
||||
B-->>F: Сохранённая страница
|
||||
else Нужна загрузка
|
||||
B->>T: GetOperationsByCursor, только исполненные операции
|
||||
T-->>B: Страница операций
|
||||
B->>B: Нормализует записи и применяет фильтры
|
||||
B-->>F: Записи и продолжение либо ошибка
|
||||
end
|
||||
```
|
||||
|
||||
`GET /api/v1/brokerage-accounts/:accountId/operations` требует Bearer token.
|
||||
Период, категории, необязательный UID, курсор и размер страницы задают запрос;
|
||||
ответ содержит записи, время наблюдения и сведения о продолжении.
|
||||
Точные поля и допустимые значения задаёт [HTTP API](../../architecture/http-api.md).
|
||||
|
||||
Backend хранит не более 100 страниц в памяти процесса. Свежая страница
|
||||
живёт до 30 секунд; при переполнении вытесняется давно не использовавшаяся.
|
||||
Ключ разделяет пользователя, поколение подключения, счёт, нормализованный
|
||||
период и категории, UID инструмента, курсор и размер страницы.
|
||||
Отсутствующий конец периода приводится к текущему 30-секундному интервалу.
|
||||
Текущее подключение и доступ к счёту проверяются до выдачи страницы из кэша.
|
||||
|
||||
После успешного изменения подключения страницы предыдущего поколения,
|
||||
в том числе поздно завершившиеся запросы, не выдаются журналу или аналитике
|
||||
дохода. Они удаляются по обычным ограничениям времени и объёма.
|
||||
Локальной синхронизации и долговременного хранения операций нет.
|
||||
Продолжения [общего журнала](../portfolio/all-accounts.md#общая-пагинация-операций)
|
||||
имеют собственные ограничения и восстановление после истечения.
|
||||
|
||||
## Представление и ошибки
|
||||
|
||||
Полный журнал и превью используют одинаковую компактную строку: логотип
|
||||
или буквенная заглушка, описание, дата, инструмент и денежное движение.
|
||||
Для купона и дивиденда денежная метка называет точное событие; для прочих
|
||||
операций — категорию. Она показывается один раз, без повтора рядом с датой.
|
||||
Знак и цвет обозначают направление денег; отсутствие движения нейтрально.
|
||||
Если описание отсутствует, показывается локализованный тип операции.
|
||||
|
||||
Полный журнал не выводит цену, количество, НКД и комиссию; личная история
|
||||
инструмента добавляет количество, цену и ссылку на счёт. Даты показываются
|
||||
как `ДД.ММ.ГГГГ`, время операций явно обозначено UTC.
|
||||
|
||||
При первом открытии виден каркас фильтров и строк. Уже полученный результат
|
||||
не заменяется каркасом при применении действий или фоновом обновлении.
|
||||
Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`)
|
||||
и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md).
|
||||
Пустой успешный ответ означает отсутствие подходящих операций, а не сбой.
|
||||
|
||||
Согласованное поведение описано в [спецификации журнала](../../../specs/openspec/specs/brokerage-account-ledger/spec.md).
|
||||
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
|
||||
счёта; их механизм раскрыт на страницах инструментов и общего портфеля.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Портфель по всем счетам
|
||||
|
||||
Режим «Все счета» объединяет открытые поддерживаемые инвестиционные счета,
|
||||
доступные через текущее подключение T-Bank. Состав совпадает с политикой
|
||||
[списка счетов](../accounts/overview.md); закрытые и недоступные счета не включаются.
|
||||
`all` — зарезервированное обозначение общего режима, которое не передаётся
|
||||
брокеру как идентификатор счёта.
|
||||
|
||||
## Маршруты и навигация
|
||||
|
||||
| Маршрут | Содержание |
|
||||
| --- | --- |
|
||||
| `/accounts` | Список отдельных счетов и действие открытия общего портфеля |
|
||||
| `/accounts/all` | Общий портфель и превью операций, дохода и будущих событий |
|
||||
| `/accounts/all/operations` | Общий журнал исполненных операций |
|
||||
| `/accounts/all/income` | Купоны и дивиденды за календарный период |
|
||||
| `/accounts/all/events` | Будущие события всех доступных счетов |
|
||||
|
||||
Общий режим использует те же страницы разделов с обозначением `all`.
|
||||
На страницах счёта и общего режима переключатель «Счёт»
|
||||
сохраняет текущий раздел и применённые URL-фильтры. При переключении пагинация
|
||||
начинается заново. Журнал хранит применённые `from`, `to` и `categories` в URL;
|
||||
черновые значения применяются кнопкой. Раздел дохода сохраняет свой прежний
|
||||
календарный контракт. Хлебные крошки называют общий режим «Все счета».
|
||||
|
||||
## Данные и вычисления
|
||||
|
||||
Backend публикует статические маршруты под `/api/v1/brokerage-accounts/all`:
|
||||
`/portfolio`, `/operations`, `/income-summary`, `/income-analytics`, `/events`.
|
||||
У них те же формы данных и фильтры, что у соответствующих разделов отдельного
|
||||
счёта. Позиции, операции (включая выплаты дохода) и события сохраняют источник
|
||||
в поле `account: { id, name }`; в ответах одного счёта это поле отсутствует.
|
||||
Валютные итоги и деньги агрегированы. Точную форму транспорта задаёт
|
||||
[OpenAPI backend](../../architecture/http-api.md).
|
||||
|
||||
- Портфель и доступные денежные остатки суммируются сервером точно, по исходной
|
||||
валюте, с точной десятичной арифметикой. FX-конвертации нет; правила
|
||||
неизвестных значений и состава сумм наследуются от [портфеля](data-reference.md).
|
||||
- Одинаковые бумаги разных счетов показаны одной карточкой с общим названием
|
||||
и идентификатором. Внутри всегда видны строки счетов: ссылка на портфель,
|
||||
количество, доступная стоимость, результат, средняя цена и доля позиции.
|
||||
Денежные показатели и количества остаются исходными для каждого счёта;
|
||||
общая средняя цена и суммарное количество карточки не рассчитываются.
|
||||
Группировка выполняется по типу инструмента и первому доступному
|
||||
идентификатору: UID, затем FIGI, затем ISIN. Разные выбранные идентификаторы
|
||||
или их отсутствие, а также одно лишь совпадение имени или тикера не
|
||||
объединяют позиции. Повторные строки одного счёта остаются отдельными.
|
||||
API сохраняет позиции каждого счёта; объединяется только их представление.
|
||||
Порядок карточек задаёт первое вхождение
|
||||
бумаги; порядок счетов внутри карточки сохраняется. Одиночные позиции
|
||||
используют прежнюю карточку. Доля относится к позициям той же валюты
|
||||
во всём показанном портфеле, без денежных остатков.
|
||||
- Журнал объединяет операции по убыванию времени, при равном времени использует
|
||||
порядок идентификаторов счетов, сохраняя порядок внутри одного счёта. ID
|
||||
операций разных счетов не считаются дублями. Переводы не схлопываются.
|
||||
- Доходы собираются из всех страниц операций каждого счёта за общие периоды.
|
||||
Итоги, сравнения, временные интервалы и принадлежность выплат рассчитывает
|
||||
сервис дохода.
|
||||
Для сравнения с годом ранее используется текущий состав счетов; новый счёт
|
||||
добавляет только реально полученные выплаты. При известных датах открытия всех счетов
|
||||
доступность сравнения определяется самым ранним открытием в этом составе.
|
||||
Закрытые счета в историческое сравнение не добавляются.
|
||||
- Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта.
|
||||
Прогноз не включается в реализованные доходы.
|
||||
|
||||
## Полнота, свежесть и доступ
|
||||
|
||||
Для каждого общего запроса backend определяет состав доступных счетов
|
||||
и единое поколение подключения. Получение данных использует эту же границу подключения;
|
||||
проверки сессии, доступа и существующий лимитер внешних запросов сохраняются.
|
||||
Финансовые данные не сохраняются в БД; снимки и продолжения хранятся
|
||||
в памяти процесса.
|
||||
|
||||
Ошибка источника не превращается в частичный успешный ответ раздела.
|
||||
Портфель может использовать разрешённый существующей политикой запасной снимок
|
||||
отдельного счёта. Тогда общий `isStale` равен `true`, а `lastSuccessfulAt`
|
||||
показывает самое раннее время среди включённых снимков. Для пустого состава
|
||||
возвращается корректный пустой результат. Общий запрос не является атомарным
|
||||
наблюдением всех счетов в один момент времени.
|
||||
|
||||
Ошибка блока операций, доходов или будущих событий не скрывает основной
|
||||
портфель. При фоновом обновлении страницы уже полученный результат сохраняется
|
||||
с индикатором или предупреждением и повторной попыткой. Смена сессии или
|
||||
подключения сбрасывает общий режим вместе с остальными финансовыми запросами.
|
||||
|
||||
## Общая пагинация операций
|
||||
|
||||
Сервис объединяет потоки страниц каждого счёта и сохраняет непрочитанные строки
|
||||
между запросами. Фильтрованные пустые страницы не обрывают поток. Повреждённая
|
||||
цепочка курсоров и нарушенный порядок дат возвращают ошибку без частичной страницы.
|
||||
|
||||
Курсор общего журнала — случайный непрозрачный идентификатор продолжения,
|
||||
привязанного к пользователю, поколению подключения, составу счетов, фильтрам
|
||||
и размеру страницы. Верхняя граница периода фиксируется первым запросом.
|
||||
Состояние ограничено 100 продолжениями на процесс со сроком жизни пять минут;
|
||||
повторный запрос того же продолжения возвращает тот же успешный результат.
|
||||
|
||||
Неизвестный, вытесненный, истёкший либо несовместимый курсор возвращает
|
||||
`400 INVALID_OPERATIONS_CURSOR`. Действие «Повторить» сбрасывает пагинацию,
|
||||
обновляет первую страницу и сохраняет фильтры. Перезапуск backend также
|
||||
инвалидирует продолжения. Раздел рассчитан на один экземпляр backend, как
|
||||
существующий финансовый кэш.
|
||||
|
||||
## Совместимость и границы требований
|
||||
|
||||
Общий режим добавляет маршруты, сохраняя маршруты отдельных счетов.
|
||||
При совместном обновлении сначала нужен backend, затем frontend с
|
||||
соответствующим сгенерированным контрактом; прежний клиент продолжает
|
||||
работать с отдельными счетами. При откате сначала возвращается клиент.
|
||||
Этот механизм не требует изменения схемы SQLite и миграции финансовых данных.
|
||||
|
||||
Основные спецификации [портфеля](../../../specs/openspec/specs/portfolio-view/spec.md),
|
||||
[операций](../../../specs/openspec/specs/brokerage-account-ledger/spec.md),
|
||||
[дохода](../../../specs/openspec/specs/brokerage-account-income-analytics/spec.md)
|
||||
и [событий](../../../specs/openspec/specs/brokerage-account-upcoming-events/spec.md)
|
||||
описывают отдельный счёт. Общий режим подтверждён действующими маршрутизаторами,
|
||||
сервисами и [проверками агрегации](../../../backend/src/tbank/all-accounts.spec.ts),
|
||||
но не выделен в самостоятельную основную спецификацию. Не следует считать
|
||||
описание общего режима изменением требований к отдельному счёту.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Данные и показатели портфеля
|
||||
|
||||
Этот справочник объясняет значения снимка одного счёта. Он нужен при чтении
|
||||
экрана, API-ответа или разборе расхождения сумм. Для
|
||||
[общего портфеля](all-accounts.md) действуют те же денежные правила,
|
||||
но данные счетов получаются не одновременно.
|
||||
|
||||
## Точные значения и неизвестность
|
||||
|
||||
Деньги представлены значением Money: ISO-код валюты и десятичная строка,
|
||||
например `{ "currency": "RUB", "value": "12500.5" }` — условный пример.
|
||||
Количества также передаются десятичными строками. Backend выполняет
|
||||
десятичную арифметику без промежуточного `Number`, нормализует отрицательные
|
||||
целую и дробную части и убирает незначащие конечные нули. Экспоненциальной
|
||||
записи нет. Frontend форматирует полученные строки в русской локали без
|
||||
сокращения, округления сумм и потери присланных дробных цифр.
|
||||
|
||||
`null` означает, что показатель нельзя определить из доступных данных.
|
||||
Это не ноль. Известная нулевая цена отличается от отсутствующей;
|
||||
позиция с ненулевым количеством и неизвестной стоимостью остаётся в составе.
|
||||
Несколько валют никогда не складываются в одну общую стоимость.
|
||||
|
||||
## Состав и денежные показатели
|
||||
|
||||
| Понятие или показатель | Смысл и источник | Ограничение |
|
||||
| --- | --- | --- |
|
||||
| Позиция | Ненулевое количество обычного инструмента из `GetPortfolio.positions` | Виртуальные позиции не включаются; порядок источника сохраняется |
|
||||
| `quantity` | Количество из текущей позиции брокера | Допустимы дробные и отрицательные количества; это не число лотов |
|
||||
| `currentValue` | Количество × текущая цена из `GetPortfolio` | `null`, если цена или её валюта недоступны |
|
||||
| `averagePurchasePrice` | Средняя цена позиции, переданная брокером | Это цена единицы, а не сумма затрат или рассчитанная приложением история покупок |
|
||||
| `absoluteReturn` | Количество × (текущая цена − средняя цена) | Требует обеих цен в одной известной валюте; не включает продажи, купоны и дивиденды |
|
||||
| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией |
|
||||
| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля |
|
||||
| `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время |
|
||||
| `isStale` | Backend вернул предыдущий полный снимок после временной ошибки | Не описывает состояние клиентского кэша или ошибку фонового запроса |
|
||||
|
||||
Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых
|
||||
инструментов в ответе отдельного счёта не объединяются; визуальная группировка
|
||||
между счетами описана [отдельно](all-accounts.md#данные-и-вычисления).
|
||||
Сводка уже включает Cash Balance: прибавление денег к ней второй раз неверно.
|
||||
|
||||
При отсутствии вычислимой стоимости у части позиций известный итог остаётся
|
||||
полезен, но представляет только оценённую часть. Известные нулевые итоги
|
||||
отдельного счёта не создают валютные карточки. Если `totalsByCurrency` пуст,
|
||||
но позиции или деньги присутствуют, это не пустой портфель.
|
||||
|
||||
## Идентификация и оформление инструмента
|
||||
|
||||
UID определяет конкретный инструмент брокера и используется для перехода
|
||||
на его страницу. FIGI, ISIN и тикер сохраняются как доступные идентификаторы;
|
||||
одно название или совпадение тикера не доказывает тождество инструментов.
|
||||
Для получения метаданных backend сначала использует UID, затем FIGI.
|
||||
Отображаемое имя выбирается в порядке: полное имя, тикер, FIGI.
|
||||
Если нет и FIGI, текущая реализация использует `Unknown instrument`.
|
||||
|
||||
ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда,
|
||||
а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения
|
||||
или метаданных не меняет количество, суммы и свежесть снимка и не скрывает
|
||||
финансовые данные. Имя ожидается перед завершением нового снимка;
|
||||
ошибка поиска оставляет доступный идентификатор.
|
||||
|
||||
## Структура и доли
|
||||
|
||||
`structure` распределяет стоимость по четырём категориям: акции (`STOCK`),
|
||||
облигации (`BOND`), прочие инструменты (`OTHER`) и деньги (`CASH`).
|
||||
Торгуемые валютные инструменты входят в `OTHER`; денежные остатки — в `CASH`.
|
||||
Сервер строит структуру из позиций и денег, без повторного добавления
|
||||
`totalsByCurrency`.
|
||||
|
||||
Доля категории — её стоимость / известная стоимость позиций и денег
|
||||
в выбранной валюте × 100. Backend округляет процент до шести знаков;
|
||||
frontend показывает до сотых. Положительное значение меньше 0,01%
|
||||
отображается как «< 0,01 %», если серверное округление не превратило его в ноль.
|
||||
В геометрию диаграммы передаются только числовые проценты; суммы в списке
|
||||
и подсказке форматируются из исходных точных строк.
|
||||
|
||||
Доля на карточке отдельной позиции использует другой знаменатель:
|
||||
сумму стоимостей всех позиций той же валюты **без денег**. Это приблизительный
|
||||
показатель для отображения, рассчитанный frontend. Он не изменяет Money.
|
||||
В режиме всех счетов знаменатель охватывает все показанные счета.
|
||||
|
||||
| Состояние | Доступное представление |
|
||||
| --- | --- |
|
||||
| Все составляющие оценены, неотрицательны, итог положителен | Суммы, доли и кольцевая диаграмма выбранной валюты |
|
||||
| Есть позиция без оценки | `unvaluedCategories` содержит её категорию и количество; суммы известной части остаются, доли и кольцо недоступны для всех валют |
|
||||
| Есть отрицательная составляющая | Суммы со знаком остаются; доли и кольцо отключены для её валюты, даже если итог категории положителен |
|
||||
| Состав пуст или имеет нулевую стоимость | Текстовое состояние вместо кольца |
|
||||
|
||||
Неоценённой позиции нельзя достоверно назначить валюту по её стоимости,
|
||||
поэтому ограничение долей действует на все валюты. В общем портфеле проверка
|
||||
отрицательных денег выполняется по остаткам счетов до взаимозачёта.
|
||||
|
||||
Нормативные основы: [нормализованный состав](../../../specs/openspec/specs/portfolio-view/spec.md#requirement-нормализованный-состав-portfolio)
|
||||
и [точные значения и валютные агрегаты](../../../specs/openspec/specs/portfolio-view/spec.md#requirement-точные-значения-и-валютные-агрегаты).
|
||||
Структура и общий режим подтверждены реализацией и её проверками;
|
||||
отдельных требований к ним в основной спецификации портфеля пока нет.
|
||||
Поля транспорта следует сверять с [HTTP API](../../architecture/http-api.md),
|
||||
а свежесть — с [потоком загрузки](flows/portfolio-loading.md).
|
||||
@@ -0,0 +1,126 @@
|
||||
# Загрузка портфеля
|
||||
|
||||
Поток начинается при открытии `/accounts/:accountId` пользователем
|
||||
с действующей сессией. Его результат — полный нормализованный снимок
|
||||
доступного счёта либо объяснимая ошибка. Браузер обращается к backend;
|
||||
доступ к брокерскому токену и внешние запросы остаются на сервере.
|
||||
|
||||
## Основной обмен
|
||||
|
||||
Сначала frontend проверяет свой успешный кэш: в течение 30 секунд повторное
|
||||
открытие может показать ответ без нового HTTP-запроса. Следующая схема
|
||||
описывает запрос, который действительно дошёл до backend.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as Frontend
|
||||
participant B as Backend API
|
||||
participant D as SQLite
|
||||
participant T as T-Bank
|
||||
F->>B: Запрос портфеля счёта с Bearer token
|
||||
B->>B: Проверяет сессию
|
||||
B->>D: Читает текущее подключение пользователя
|
||||
D-->>B: Подключение либо отсутствие
|
||||
alt Подключения нет
|
||||
B-->>F: 409 TBANK_NOT_CONNECTED
|
||||
else Подключение доступно
|
||||
B->>B: Определяет поколение подключения
|
||||
alt Снимок того же поколения не старше 30 секунд
|
||||
B-->>F: Полный снимок, isStale=false
|
||||
else Нужен новый снимок
|
||||
B->>T: Получает открытые счета
|
||||
T-->>B: Состав доступных счетов
|
||||
alt Счёт недоступен
|
||||
B-->>F: 404
|
||||
else Счёт доступен
|
||||
par Финансовые позиции
|
||||
B->>T: GetPortfolio
|
||||
T-->>B: Позиции и цены
|
||||
and Доступные деньги
|
||||
B->>T: GetPositions
|
||||
T-->>B: Денежные остатки
|
||||
end
|
||||
B->>T: Метаданные отсутствующих инструментов по UID, затем FIGI
|
||||
T-->>B: Метаданные, отсутствие или ошибка
|
||||
B->>B: Нормализует состав и точно вычисляет суммы
|
||||
B-->>F: Полный новый снимок, isStale=false
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Свежий серверный снимок выдаётся после разрешения текущего сохранённого
|
||||
подключения, но без нового `GetAccounts`. После истечения свежего кэша
|
||||
проверка доступности счёта предшествует финансовым RPC. SQLite хранит
|
||||
подключение, а не снимок или право владения счётом вне этого подключения.
|
||||
|
||||
Для нового снимка требуются оба финансовых ответа. Backend не выдаёт
|
||||
позиции без денег или деньги без позиций как полный портфель.
|
||||
Метаданные дополняют имя и идентификаторы, но их ошибка не срывает
|
||||
финансовый результат. Общий серверный клиент обеспечивает TLS, очередь,
|
||||
ограничение частоты, deadline RPC и разрешённые повторные попытки —
|
||||
[граница интеграции](../../../integrations/tbank.md).
|
||||
|
||||
## Время и изменение подключения
|
||||
|
||||
| Слой | Что сохраняется | Условие повторного использования |
|
||||
| --- | --- | --- |
|
||||
| Frontend | Последний успешный HTTP-ответ для выбранного счёта и сессии | Свежим считается 30 секунд; при обычном обновлении уже показанный ответ остаётся видимым |
|
||||
| Backend | Полный снимок пары пользователь–счёт в памяти процесса | До 30 секунд, только в том же поколении текущего подключения |
|
||||
| Backend при временной ошибке | Тот же полный снимок | Не старше пяти минут с момента успешного сохранения, только в том же поколении |
|
||||
| Метаданные инструмента | Успешный поиск или подтверждённое отсутствие | До 24 часов; общий ограниченный кэш на 500 ключей UID/FIGI, ошибки не сохраняются |
|
||||
|
||||
Это параметры текущей реализации, а не самостоятельная политика этого
|
||||
документа: [снимки](../../../../backend/src/tbank/portfolio.service.ts),
|
||||
[метаданные](../../../../backend/src/tbank/instrument-metadata.service.ts) и
|
||||
[клиентский запрос](../../../../frontend/src/entities/portfolio/api/usePortfolio.ts).
|
||||
|
||||
Пять минут — предельный возраст успешного снимка, а не дополнительные
|
||||
пять минут после окончания свежих 30 секунд. Запасной ответ сохраняет
|
||||
исходное `lastSuccessfulAt` и получает `isStale: true`.
|
||||
|
||||
Успешное создание, замена или удаление подключения меняет границу
|
||||
повторного использования финансовых данных. Старое или поздно завершившееся
|
||||
получение снимка не делает его доступным новым запросам другого поколения.
|
||||
Явная очистка памяти при изменении подключения дополняет эту проверку.
|
||||
Frontend сбрасывает всё поддерево финансовых запросов при изменении
|
||||
подключения и общий кэш сессии при выходе; старый портфель не служит
|
||||
заглушкой загрузки нового подключения.
|
||||
|
||||
В общем режиме сначала определяется состав доступных счетов одного
|
||||
подключения, затем получаются их снимки. Это
|
||||
[не атомарное наблюдение нескольких счетов](../all-accounts.md#полнота-свежесть-и-доступ).
|
||||
|
||||
## Ошибки и результат для пользователя
|
||||
|
||||
| Причина | Ответ backend | Действие на экране |
|
||||
| --- | --- | --- |
|
||||
| Нет подключения | `409 TBANK_NOT_CONNECTED` | Открыть настройки T-Bank |
|
||||
| Брокер отклонил токен | `400 INVALID_TBANK_TOKEN` | Открыть настройки; токен автоматически не удаляется |
|
||||
| Счёт недоступен через текущее подключение | `404` | Вернуться к списку счетов |
|
||||
| Временная ошибка, пригодный предыдущий снимок есть | `200`, `isStale: true` | Показать снимок и его исходное время с предупреждением |
|
||||
| Временная ошибка, пригодного снимка нет | `503 TBANK_UNAVAILABLE` | Повторить загрузку |
|
||||
| Неизвестная ошибка | Безопасная ошибка API | Повторить загрузку |
|
||||
|
||||
`INVALID_TBANK_TOKEN` и `404` не заменяются серверным запасным снимком.
|
||||
Временная ошибка определения доступных счетов также может использовать
|
||||
пригодный снимок того же поколения. Сбой метаданных использует идентификатор
|
||||
инструмента и не устанавливает `isStale`.
|
||||
|
||||
На клиенте нет polling, автоматического повтора неуспешного запроса или
|
||||
обновления при возврате фокуса. При обновлении того же уже показанного
|
||||
результата клиент оставляет его на экране, добавляя индикатор загрузки
|
||||
или предупреждение с доступной с клавиатуры повторной попыткой.
|
||||
Это клиентский сохранённый ответ, независимо от серверного `isStale`.
|
||||
|
||||
Первое обращение к крупному портфелю может потребовать много метаданных.
|
||||
Переполнение общей очереди RPC означает временную недоступность конкретного
|
||||
поиска. После успешного поиска метаданные используются повторно до истечения
|
||||
кэша. Значение `TBANK_RATE_LIMIT_PER_SECOND` следует соотносить с квотой
|
||||
источника; его увеличение не является гарантированным исправлением
|
||||
ограничения самого T-Bank.
|
||||
|
||||
Проверяемые правила: [свежесть и запасной снимок](../../../../specs/openspec/specs/portfolio-view/spec.md#requirement-свежесть-и-fallback-полного-portfolio-snapshot),
|
||||
[клиентские состояния](../../../../specs/openspec/specs/portfolio-view/spec.md#requirement-состояния-и-ошибки-portfolio-на-клиенте).
|
||||
Для расследования конкретного значения используйте
|
||||
[порядок диагностики](../how-to/investigate-discrepancy.md).
|
||||
@@ -0,0 +1,56 @@
|
||||
# Разобрать расхождение суммы портфеля
|
||||
|
||||
Используйте этот порядок, когда итог, позиция или доля на экране отличаются
|
||||
от ожидаемого значения. Он помогает определить слой расхождения без изменения
|
||||
данных. Нужны адрес экрана и уже доступный ответ API либо синтетический пример
|
||||
из существующей проверки. Реальные токены, персональные выгрузки и содержимое
|
||||
базы для разбора не нужны; не переносите их в документацию или отчёт.
|
||||
|
||||
1. **Определите предмет сравнения.** Запишите, открыт отдельный счёт или
|
||||
«Все счета», какая валюта и какой показатель расходятся. Текущая стоимость,
|
||||
свободные деньги, результат открытой позиции и полученный доход — разные
|
||||
величины. Убедитесь, что сравниваемые значения относятся к одному составу
|
||||
счетов и инструменту. Название или тикер сами по себе не доказывают
|
||||
совпадение выпуска; используйте доступные UID, FIGI и ISIN.
|
||||
2. **Сопоставьте время.** Проверьте `lastSuccessfulAt`, `isStale` и наличие
|
||||
предупреждения о клиентском обновлении. Серверный снимок не обещает время
|
||||
каждой котировки, а общий портфель может соединять разные моменты.
|
||||
Если запрос завершился ошибкой, доступное действие «Повторить» должно
|
||||
обновить результат или оставить объяснимую ошибку. Перезагрузка backend
|
||||
удаляет кэш и не восстанавливает историю стоимости.
|
||||
3. **Проверьте состав суммы.** Для одной валюты итог складывается из
|
||||
вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги
|
||||
второй раз, не складывайте валюты и не включайте заблокированные средства.
|
||||
Ненулевая позиция с `currentValue: null` делает оценку неполной, но не
|
||||
даёт нулевой вклад в смысле полной стоимости. `CURRENCY` участвует
|
||||
в суммах, хотя текущий интерфейс скрывает её карточку —
|
||||
[известное расхождение со спецификацией](../overview.md#как-читать-экран).
|
||||
4. **Проверьте показатель по его определению.** Используйте
|
||||
[справочник](../data-reference.md#состав-и-денежные-показатели).
|
||||
Условный пример: 2 бумаги по 100 RUB и 50 RUB свободных денег дают
|
||||
250 RUB стоимости. Доля позиции среди позиций равна 100%, а доля
|
||||
соответствующей категории в структуре с деньгами — 80%.
|
||||
Купоны и дивиденды не включаются в результат открытой позиции.
|
||||
Для проверки десятичных сумм сохраняйте исходные строки;
|
||||
арифметика с плавающей точкой может сама создать расхождение.
|
||||
5. **Найдите ответственную границу.** Если ответ API корректен, а экран
|
||||
теряет цифры, валюту или показатель, исследуйте frontend.
|
||||
Если исходные безопасные данные и нормализованный ответ расходятся,
|
||||
исследуйте backend. Если не совпадают ожидаемое и описанное поведение,
|
||||
сначала сопоставьте [спецификацию портфеля](../../../../specs/openspec/specs/portfolio-view/spec.md)
|
||||
и ограничения [потока загрузки](../flows/portfolio-loading.md).
|
||||
Исправление документации само по себе не разрешает конфликт требований
|
||||
и реализации.
|
||||
|
||||
Для адресной проверки исходников владельцами служат
|
||||
[сервис снимков](../../../../backend/src/tbank/portfolio.service.ts),
|
||||
[валютные итоги](../../../../backend/src/tbank/portfolio-totals.ts),
|
||||
[структура](../../../../backend/src/tbank/portfolio-structure.ts) и
|
||||
[подготовка экрана](../../../../frontend/src/pages/account/ui/AccountPage.vm.ts).
|
||||
Это точки продолжения расследования; смысл вычислений изложен выше.
|
||||
|
||||
Разбор завершён, когда различие объяснено составом, временем или определением
|
||||
показателя либо получен воспроизводимый пример дефекта с ожидаемым и
|
||||
фактическим результатом. В последнем случае сохраните минимальный синтетический
|
||||
пример и адрес ответственного слоя. Ошибки источника и неизвестные значения
|
||||
не исправляются подстановкой нулей или обходом проверки доступа.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Портфель
|
||||
|
||||
Портфель показывает, какими активами и доступными деньгами располагает
|
||||
брокерский счёт по последнему успешному наблюдению T-Bank. Это снимок состава
|
||||
и его текущей оценки, а не локальная история владения или расчёт полной
|
||||
инвестиционной доходности. Пользователь открывает его на
|
||||
`/accounts/:accountId`; [режим «Все счета»](all-accounts.md) объединяет
|
||||
доступные счета текущего подключения.
|
||||
|
||||
## Снимок и связанные области
|
||||
|
||||
Снимок состоит из позиций по инструментам, доступных денежных остатков,
|
||||
валютных итогов и структуры активов. Позиция означает ненулевое количество
|
||||
обычного инструмента в брокерском портфеле. Торгуемая валютная позиция
|
||||
`CURRENCY` и свободные деньги имеют разный смысл и передаются раздельно.
|
||||
Точный смысл сумм и неизвестных значений раскрыт в
|
||||
[справочнике данных](data-reference.md).
|
||||
|
||||
Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md).
|
||||
SQLite не хранит сам портфель: backend получает данные через серверную
|
||||
[интеграцию T-Bank](../../integrations/tbank.md) и временно сохраняет успешный
|
||||
снимок в памяти. Frontend получает нормализованный результат через HTTP API.
|
||||
[Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа,
|
||||
свежий кэш и восстановление после временной ошибки.
|
||||
|
||||
Обзор счёта также показывает пять последних [операций](../operations/overview.md),
|
||||
[полученный доход](../income/overview.md) текущего месяца и до пяти
|
||||
[будущих событий](../income/upcoming-events.md). Каждый блок загружается
|
||||
самостоятельно: его ошибка или отсутствие данных не скрывает основной
|
||||
портфель. Полученные выплаты и прогноз не добавляются к текущей стоимости
|
||||
повторно и не превращают результат открытой позиции в полную доходность.
|
||||
|
||||
## Как читать экран
|
||||
|
||||
Валютная сводка показывает отдельную карточку для каждого полученного итога.
|
||||
Деньги уже включены в итог своей валюты. RUB, USD и EUR обозначены ₽, $ и €;
|
||||
другие валюты сохраняют ISO-код. Вспомогательным технологиям доступны полное
|
||||
значение и название валюты. Суммы сохраняют все исходные цифры, знак и
|
||||
дробные позиции, включая `-0,5`; длинные значения переносятся без обрезания.
|
||||
Карточки не интерактивны и занимают одну колонку на узком экране,
|
||||
две или три на широком. Отсутствующая валюта не получает искусственную
|
||||
нулевую карточку. Пустые итоги не скрывают доступный состав портфеля.
|
||||
|
||||
Структура после сводки распределяет известную стоимость по акциям,
|
||||
облигациям, другим активам и свободным деньгам. При нескольких валютах
|
||||
пользователь выбирает одну; при одной отдельного переключателя нет.
|
||||
Кольцевая диаграмма дополнена текстовым списком сумм и долей и подсказкой;
|
||||
на узком экране список расположен под ней. Неизвестная оценка, отрицательные
|
||||
составляющие или нулевой итог ограничивают возможность показать доли —
|
||||
[почему кольцо бывает недоступно](data-reference.md#структура-и-доли).
|
||||
|
||||
Акции и облигации показаны отдельными секциями, по одной карточке в ряду
|
||||
при любой ширине. Внутри сохраняется порядок источника. Слева находятся
|
||||
название, логотип, количество и идентификатор, справа — стоимость и результат.
|
||||
Средняя цена и доля размещены ниже. Логотип размером 40 px при недоступности
|
||||
изображения заменяется первой буквой имени. Акция показывает тикер, а при его
|
||||
отсутствии ISIN; облигация — ISIN. Название и логотип ведут на страницы
|
||||
[акции](../instruments/stocks.md) или [облигации](../instruments/bonds.md),
|
||||
когда доступен точный UID инструмента.
|
||||
|
||||
Результат открытой позиции имеет явный знак: плюс при положительном,
|
||||
минус при отрицательном, без знака при нуле. Цвет дополняет знак.
|
||||
Иные отображаемые активы используют компактные плитки в адаптивной сетке
|
||||
без логотипа и с подписью результата. Отсутствующие показатели скрываются,
|
||||
а не подменяются нулём или заглушкой.
|
||||
|
||||
Существует расхождение: [согласованная спецификация представления позиций](../../../specs/openspec/specs/portfolio-view/spec.md#requirement-представление-карточек-позиций-на-клиенте)
|
||||
требует доступности всех иных типов в отдельной секции, а текущие frontend
|
||||
и его проверка исключают `CURRENCY` только из списка карточек. Такие позиции
|
||||
остаются в ответе, валютных итогах, структуре и знаменателе долей остальных
|
||||
позиций. Это описание фактического ограничения; выбор между изменением
|
||||
интерфейса и согласованием требований остаётся отдельной задачей.
|
||||
|
||||
## Свежесть и границы
|
||||
|
||||
При первом запросе экран показывает каркас заголовка, сводки и разделов.
|
||||
Три формы валютных карточек в каркасе не обещают наличие трёх валют.
|
||||
Загрузка обозначена для вспомогательных технологий; декоративные элементы
|
||||
не содержат данных и не получают фокус. При обычном фоновом обновлении
|
||||
сохраняются предыдущие суммы и время успешного получения в UTC.
|
||||
|
||||
Серверный `isStale` обозначает разрешённый запасной снимок backend.
|
||||
Клиентское предупреждение означает неудачную попытку обновить уже показанный
|
||||
ответ. Эти состояния независимы и могут появиться одновременно.
|
||||
Успешный повторный запрос обновляет суммы и время, снимает клиентскую ошибку;
|
||||
серверное предупреждение определяется новым `isStale`.
|
||||
Правила времени и ошибок приведены в [потоке загрузки](flows/portfolio-loading.md).
|
||||
|
||||
В текущем решении нет долговременной истории снимков, графиков стоимости
|
||||
портфеля, валютной конвертации, учёта виртуальных позиций или заблокированных
|
||||
средств. Кэш теряется при перезапуске и рассчитан на локальную работу одного
|
||||
экземпляра backend. Отдельной команды принудительного обновления на экране
|
||||
нет; повторная попытка доступна после ошибки.
|
||||
|
||||
Для проверки конкретной суммы используйте
|
||||
[руководство по расхождениям](how-to/investigate-discrepancy.md).
|
||||
Проверяемое поведение отдельного счёта описано в
|
||||
[спецификации портфеля](../../../specs/openspec/specs/portfolio-view/spec.md);
|
||||
точную форму HTTP-ответа задаёт [OpenAPI backend](../../architecture/http-api.md).
|
||||
Reference in New Issue
Block a user