diff --git a/features/portfolio-view.md b/features/portfolio-view.md new file mode 100644 index 0000000..36ffabd --- /dev/null +++ b/features/portfolio-view.md @@ -0,0 +1,59 @@ +# Портфель + +Авторизованный пользователь просматривает текущий состав одного доступного брокерского счёта на `/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. Успешный ответ содержит `lastSuccessfulAt` в RFC 3339 UTC, `isStale`, `totalsByCurrency`, `positions` и `cashBalances`. + +Все количества и суммы передаются decimal strings без экспоненциальной записи. Деньги содержат ISO-код валюты. Backend выполняет точные операции через `BigInt`; frontend только форматирует строки для русской локали и не выполняет финансовую арифметику. + +`Portfolio Position` -- ненулевой обычный актив из T-Bank portfolio. `currentValue` равен количеству, умноженному на текущую цену, когда оба значения и валюта доступны. `absoluteReturn` доступен только при одинаковой известной валюте текущей и средней цены. Невычислимые значения передаются `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 и не скрывают финансовые данные. + +Первый запрос крупного 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 при возврате фокуса. После устаревания предыдущий результат скрывается до следующего ответа. Акции и облигации отображаются отдельными списками с upstream-порядком внутри каждого; остальные активы остаются в отдельной секции. RUB форматируется как `₽`. Loaded view сохраняет адаптивное одноколоночное отображение на узком экране. + +## Ограничения MVP + +In-memory cache теряется после перезапуска и рассчитан на одного User. Нет истории Snapshot, операций, графиков, валютной конвертации, виртуальных позиций, заблокированных средств, ручного обновления и поддержки нескольких backend instances. diff --git a/features/tbank-connection.md b/features/tbank-connection.md index c53a97a..971d2d3 100644 --- a/features/tbank-connection.md +++ b/features/tbank-connection.md @@ -28,10 +28,12 @@ sequenceDiagram - `TOKEN_ENCRYPTION_KEY` - закодированный в base64 32-байтный ключ AES-256-GCM; обязателен при запуске. - `TBANK_GRPC_ENDPOINT` по умолчанию имеет значение `invest-public-api.tbank.ru:443`. -- `TBANK_RATE_LIMIT_PER_SECOND` по умолчанию имеет значение `5`. +- `TBANK_RATE_LIMIT_PER_SECOND` задаёт максимальное число T-Bank RPC в секунду на экземпляр backend; значение по умолчанию -- `5`. - `TBANK_CA_CERT_PATH` - необязательный путь к корневому CA-сертификату PEM для сетей с TLS-инспекцией; проверка TLS остается включенной. -FIFO-ограничитель на экземпляр запускает не более 5 запросов в секунду и принимает не более 50 ожидающих вызовов. Для `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED` выполняется одна повторная попытка через секунду. Deadline каждой попытки - 10 секунд. +FIFO-ограничитель на экземпляр запускает не более 5 запросов в секунду и принимает не более 50 ожидающих вызовов. Для `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED` выполняется одна повторная попытка через секунду. Deadline каждой попытки - 15 секунд. + +Портфель с большим числом ранее не кэшированных инструментов создаёт отдельные metadata lookup. Если в такой ситуации очередь переполняется и позиции получают FIGI вместо names, увеличьте `TBANK_RATE_LIMIT_PER_SECOND` в соответствии с разрешённой upstream-квотой; для локального портфеля из множества позиций использовалось значение `50`. ## MVP limits