18 KiB
Портфель
Авторизованный пользователь просматривает текущий Portfolio Snapshot одного Brokerage Account, доступного через текущее T-Bank connection, на /accounts/:accountId. Frontend обращается только к backend API; T-Bank остаётся server-side интеграцией.
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant D as SQLite
participant T as T-Bank gRPC
U->>F: Открывает /accounts/:accountId
F->>B: GET /api/v1/brokerage-accounts/:accountId/portfolio
B->>D: Читает T-Bank connection
B->>T: GetAccounts(OPEN)
alt Счёт недоступен
B-->>F: 404
F-->>U: Состояние недоступного счёта
else Свежий Snapshot
B-->>F: 200 Portfolio isStale=false
else Требуется обновление
par Получение финансовых данных
B->>T: GetPortfolio
and
B->>T: GetPositions
end
B->>T: GetInstrumentBy(FIGI), при необходимости
B-->>F: Полный Portfolio или Problem Details
end
API
GET /api/v1/brokerage-accounts/:accountId/portfolio требует Bearer access token. Успешный ответ содержит Portfolio Snapshot: lastSuccessfulAt в RFC 3339 UTC, isStale, totalsByCurrency, positions и cashBalances.
Все количества и суммы передаются decimal strings без экспоненциальной записи. Деньги передаются как Money { currency, value } с ISO-кодом валюты. Backend выполняет точные денежные операции; frontend форматирует строки для русской локали и не выполняет финансовую арифметику кроме display-only оценки доли позиции.
Portfolio Position -- ненулевой обычный актив из T-Bank portfolio. Position с type CURRENCY означает торгуемый валютный Instrument и не является Cash Balance. currentValue равен количеству, умноженному на текущую цену, когда оба значения и валюта доступны. absoluteReturn доступен только при одинаковой известной валюте текущей и средней цены. instrumentUid передаётся из upstream для точной идентификации и перехода на страницу акции. isin и ticker берутся из уже разрешённой metadata Instrument и передаются nullable-строками. Невычислимые значения передаются null, а не нулём. Позиции остаются в upstream-порядке; виртуальные позиции не входят в ответ.
Cash Balance -- доступный ненулевой денежный остаток. Несколько остатков одной валюты суммируются; заблокированные средства не включаются. Итоги группируются по валюте и включают стоимость позиций и Cash Balance без конвертации.
Metadata инструмента ищется сначала по instrument UID, затем по FIGI. Отображение использует полное name, затем ticker, затем FIGI. Для нового Snapshot backend ожидает отсутствующие metadata в пределах gRPC deadline, чтобы показать name при доступном ответе T-Bank. Ошибка, отсутствие инструмента или пустые name/ticker используют FIGI как fallback и не скрывают финансовые данные. Бренд инструмента предоставляет logoName -- имя файла логотипа (Instrument Logo); оно передаётся nullable-строкой без сборки URL на backend, а frontend загружает изображение из публичного брендового CDN.
Первый запрос крупного Portfolio может выполнить много metadata lookup. Если настроенный per-instance лимит T-Bank RPC недостаточен и локальная очередь переполняется, увеличьте TBANK_RATE_LIMIT_PER_SECOND в рамках upstream-квоты; после успешного lookup metadata остаётся в 24-часовом in-memory cache.
Валютная сводка
Над составом портфеля каждый полученный валютный итог показан отдельной компактной карточкой. На узком экране карточки идут в одну колонку, на широком — в две или три. Метка и сумма расположены в одной строке, пока хватает места; длинная сумма переносится под метку и при необходимости на несколько строк без обрезания и горизонтальной прокрутки.
Для RUB, USD и EUR сводка использует знаки ₽, $ и € в метке и после суммы. Вспомогательным технологиям доступны названия «Российский рубль», «Доллар США» и «Евро» вместе с полным значением. Остальные валюты сохраняют исходный ISO-код, поэтому USD и CAD остаются различимыми. Карточки не интерактивны.
Суммы форматируются из decimal strings с пробелами между разрядами и десятичной
запятой: сохраняются все цифры, дробные нули и отрицательный знак, включая
-0,5. Округления, сокращения до тысяч или миллионов и валютной конвертации нет.
Каждый итог уже включает позиции и доступные деньги своей валюты; frontend
не добавляет остатки повторно и не считает межвалютную общую стоимость.
Позиции без вычислимой стоимости не превращаются в нулевые суммы, поэтому
сводка не обещает полной оценки портфеля. Отсутствующие валюты не дополняются
карточками; при пустом списке итогов доступный состав портфеля остаётся видимым.
Структура портфеля
После валютной сводки, перед составом портфеля, показана структура по типам: акции, облигации, другие активы и свободные деньги. Этот же блок используется в обзоре «Все счета». Mantine Charts 9.5.1 с Recharts 3.10.1 отображает кольцо и подсказку сегмента; суммы и доли всегда доступны в соседнем списке. На узком экране список расположен под кольцом. При нескольких валютах доступен Select, при одной — дополнительный control отсутствует.
Backend формирует Portfolio.structure из нормализованных позиций и денег,
используя точную decimal-арифметику. Сводные итоги повторно не прибавляются.
Тип CURRENCY относится к другим активам; денежные остатки — к свободным
деньгам. Доли учитывают деньги той же валюты, без FX. Доли на существующих
карточках позиций сохраняют прежний знаменатель без денег.
Суммы категорий передаются без потери точности. Сервер округляет процент до шести знаков, клиент отображает его до сотых; положительная доля меньше 0,01% обозначается «< 0,01 %», если она не округлилась в ноль на сервере. В графическую библиотеку передаются только числовые проценты для геометрии, а суммы в списке и подсказке форматируются из исходных decimal strings.
unvaluedCategories сохраняет типы и количество позиций без оценки. Их
валюту нельзя определить по текущей стоимости, поэтому при наличии таких
позиций кольцо и доли недоступны для всех валют. UI показывает объяснение,
известные суммы и категории без оценки, не выдавая неизвестное за ноль.
Отрицательная позиция или остаток отключает кольцо своей валюты даже при
положительной сумме категории; суммы со знаком остаются в списке. В общем
режиме проверка использует остатки отдельных счетов до их взаимозачёта.
При пустом или нулевом составе вместо кольца показано текстовое состояние.
Блок использует тот же snapshot и lifecycle обновления, что и валютная сводка.
Свежесть и ошибки
При первом открытии Portfolio frontend показывает structural skeleton заголовка,
сводки и sections. На уже отрисованном обзоре Account independently loading
recent operations и future events заменяются skeleton только внутри своего
section; Portfolio и соседние sections остаются доступными. Skeleton placeholders
декоративны, не содержат пользовательских данных и не получают keyboard focus.
Область сводки содержит три формы компактных карточек в той же адаптивной сетке;
это не обещает три валюты в ответе. Загрузка имеет доступное busy-обозначение.
При фоновом обновлении и его ошибке сводка сохраняет прежние суммы и время
успешного получения в UTC. Повторная попытка доступна с клавиатуры; новый
успешный ответ обновляет суммы и время и снимает клиентскую ошибку, а server-side
stale-предупреждение зависит от isStale нового ответа.
- Backend хранит успешный полный Snapshot для пары User и Brokerage Account свежим 30 секунд.
- До cache hit backend разрешает текущее persisted T-Bank connection. Snapshot повторно используется только внутри непрозрачной generation этого connection; при cache miss доступ к Brokerage Account проверяется до финансовых T-Bank RPC.
- После истечения fresh TTL последний Snapshot той же generation можно вернуть как fallback не более пяти минут только при
TBANK_UNAVAILABLE; такой ответ имеетisStale: trueи исходныйlastSuccessfulAt. INVALID_TBANK_TOKENникогда не маскируется fallback-данными.- Успешное создание, замена или удаление T-Bank connection меняет cache boundary. Сохранённый либо поздно завершившийся Snapshot предыдущей generation не выдаётся новому запросу; явная очистка при mutation остаётся best-effort освобождением памяти.
- Metadata по FIGI и подтверждённое отсутствие инструмента кэшируются в памяти 24 часа глобально; ошибки lookup не кэшируются.
409 TBANK_NOT_CONNECTEDи400 INVALID_TBANK_TOKENведут в настройки;404показывает возврат к списку счетов;503 TBANK_UNAVAILABLEи неизвестная ошибка позволяют повторить запрос, если нет fallback Snapshot.
Клиент использует успешный React Query-кэш 30 секунд, без polling, automatic retry и refetch при возврате фокуса. Successful connection mutation сбрасывает Portfolio query вместе со всем brokerage subtree, поэтому предыдущий Snapshot не показывается во время загрузки новой generation. При обычном фоновом обновлении без смены connection уже полученный Portfolio остаётся видимым как Client Cached Snapshot; отдельные индикатор обновления и предупреждение с повторной попыткой показывают его состояние. Это не server-side fallback: Portfolio.isStale сообщает только о fallback Snapshot от backend и может отображаться одновременно с client refresh warning. Акции и облигации отображаются отдельными секциями с upstream-порядком внутри каждой; остальные активы остаются в отдельной секции. Карточки акций и облигаций выводятся одноколоночным grid при любой ширине экрана и показывают Instrument Logo слева от текстового блока (аватар 40px с фолбэком на первую букву отображаемого имени при отсутствии logoName или ошибке загрузки), идентификатор, количество, текущую стоимость и абсолютный доход со знаком +/- без текстовой подписи (ноль -- без знака) с цветовой индикацией знака (зелёный при положительном, красный при отрицательном). Карточка акции группирует логотип, имя, количество и ticker слева; при ticker: null вместо него показывает ISIN. Карточка облигации и плитка актива иного типа сохраняют ISIN. Стоимость с доходностью остаются справа, а средняя цена и доля показываются одной приглушённой строкой под разделителем. Карточки остальных активов остаются компактными плитками в адаптивном grid -- одна колонка на узком экране, 2-3 на широком, -- с текстовой подписью абсолютного дохода и без визуального логотипа даже при наличии logoName. Плитка показывает идентификатор, количество, текущую стоимость, абсолютный доход с цветовой индикацией знака (зелёный при положительном, красный при отрицательном), среднюю цену покупки, долю среди позиций той же валюты и ISIN. Доля -- display-only оценка, вычисляемая на клиенте как currentValue позиции к сумме currentValue позиций той же валюты, без Cash Balance; отсутствующие метрики скрываются, а не отображаются заглушкой. RUB форматируется как ₽. Loaded view сохраняет адаптивное одноколоночное отображение на узком экране.
Ограничения MVP
In-memory cache теряется после перезапуска и рассчитан на одного User. Нет истории Snapshot, графиков стоимости и доходности, валютной конвертации, виртуальных позиций, заблокированных средств, ручного обновления и поддержки нескольких backend instances.