docs: reorganize system knowledge by domain

This commit is contained in:
2026-09-12 14:45:42 +03:00
parent 3a99af1a6e
commit f20c3f47bc
83 changed files with 2229 additions and 7335 deletions
+100
View File
@@ -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).
Её раздел об экране пока требует одноколоночного списка и не описывает
счётчики акций и облигаций. Текущая реализация и её проверки используют
две колонки на широком экране и показывают эти счётчики. Расхождение требует
отдельного согласования требований; в этой документации отражён наблюдаемый экран.
+122
View File
@@ -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).
+127
View File
@@ -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).
Не следует использовать календарь будущих событий для восстановления
состава уже полученного дохода.
+109
View File
@@ -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), где их смысл также разделён.
+142
View File
@@ -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).
+172
View File
@@ -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).
+117
View File
@@ -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).
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
счёта; их механизм раскрыт на страницах инструментов и общего портфеля.
+119
View File
@@ -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),
но не выделен в самостоятельную основную спецификацию. Не следует считать
описание общего режима изменением требований к отдельному счёту.
+98
View File
@@ -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).
Это точки продолжения расследования; смысл вычислений изложен выше.
Разбор завершён, когда различие объяснено составом, временем или определением
показателя либо получен воспроизводимый пример дефекта с ожидаемым и
фактическим результатом. В последнем случае сохраните минимальный синтетический
пример и адрес ответственного слоя. Ошибки источника и неизвестные значения
не исправляются подстановкой нулей или обходом проверки доступа.
+99
View File
@@ -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).