# Портфель Авторизованный пользователь просматривает текущий Portfolio Snapshot одного Brokerage Account, доступного через текущее T-Bank connection, на `/accounts/:accountId`. Frontend обращается только к backend API; T-Bank остаётся server-side интеграцией. ```mermaid 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 выполняет точные операции через `BigInt`; frontend форматирует строки для русской локали и не выполняет финансовую арифметику кроме display-only оценки доли позиции. `Portfolio Position` -- ненулевой обычный актив из T-Bank portfolio. Position с type `CURRENCY` означает торгуемый валютный Instrument и не является Cash Balance. `currentValue` равен количеству, умноженному на текущую цену, когда оба значения и валюта доступны. `absoluteReturn` доступен только при одинаковой известной валюте текущей и средней цены. `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. ## Свежесть и ошибки При первом открытии Portfolio frontend показывает structural skeleton заголовка, сводки и sections. На уже отрисованном обзоре Account independently loading recent operations и future events заменяются skeleton только внутри своего section; Portfolio и соседние sections остаются доступными. Skeleton placeholders декоративны, не содержат пользовательских данных и не получают keyboard focus. - 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.