60 lines
9.6 KiB
Markdown
60 lines
9.6 KiB
Markdown
# Портфель
|
||
|
||
Авторизованный пользователь просматривает текущий 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.
|
||
|
||
## Свежесть и ошибки
|
||
|
||
- Backend хранит успешный полный Snapshot для пары User и Brokerage Account свежим 30 секунд.
|
||
- После истечения fresh TTL последний Snapshot можно вернуть как fallback не более пяти минут только при `TBANK_UNAVAILABLE`; такой ответ имеет `isStale: true` и исходный `lastSuccessfulAt`.
|
||
- `INVALID_TBANK_TOKEN` никогда не маскируется fallback-данными.
|
||
- Успешная замена или удаление T-Bank connection очищает Portfolio Snapshot User.
|
||
- 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 при возврате фокуса. При фоновом обновлении уже полученный 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.
|