# Проектирование брокерских портфелей T-Bank **Статус:** одобрено для планирования реализации **Дата:** 2026-06-16 **Владелец:** MoexVibe ## Контекст MoexVibe уже поддерживает вручную управляемые виртуальные портфели на основе рыночных данных MOEX. Следующий шаг — интеграция T-Bank Invest только для чтения, чтобы приложение показывало реальные брокерские счета, позиции, денежные остатки и историю операций. Первая версия интеграции персональная и серверная. Бэкенд читает один `T_BANK_TOKEN` из `apps/backend/.env`. В будущих версиях это можно заменить хранением токенов на уровне пользователя, но первая реализация не должна требовать более крупной модели секретов. ## Факты о внешнем API T-Bank Invest API — это gRPC API. REST доступен как прокси, а WebSocket существует для клиентов, которым он нужен, но основным протоколом является gRPC. Production endpoint: `invest-public-api.tbank.ru:443`; sandbox endpoint: `sandbox-invest-public-api.tbank.ru:443`. Авторизация передается как gRPC-метаданные: ```text Authorization: Bearer ``` Нужные официальные сервисы и методы: | Потребность | Метод T-Bank | Примечания | | --- | --- | --- | | Счета | `UsersService/GetAccounts` | Фильтровать только открытые брокерские счета и ИИС. | | Текущая оценка портфеля | `OperationsService/GetPortfolio` | Возвращает итоги, позиции, дневную доходность, ожидаемую доходность. | | Деньги и рассчитанные позиции | `OperationsService/GetPositions` | Возвращает деньги, заблокированные деньги, бумаги, фьючерсы, опционы. | | История операций | `OperationsService/GetOperationsByCursor` | Курсорная пагинация, лимит до 1000, фильтры по типам операций. | | Метаданные инструмента | `InstrumentsService/GetInstrumentBy` | Получение `instrument_uid`, тикера, class code, лота, названия, ISIN. | Официальная документация рекомендует оставаться ниже 50 запросов в секунду суммарно по счетам и токенам. Документированные лимиты unary-запросов включают 100 запросов в минуту для сервиса счетов и 200 запросов в минуту для сервиса операций. T-Bank также возвращает метаданные по лимитам запросов, например `x-ratelimit-limit`, `x-ratelimit-remaining` и `x-ratelimit-reset`. Stream-сервисы существуют для обновлений портфеля, позиций и операций. Они пригодятся позже, но первая реализация использует unary-чтение плюс локальные кеширование и синхронизацию: это проще, тестируемее и не держит лишние долгоживущие соединения. Ссылки: - [T-Invest API intro](https://developer.tbank.ru/invest/intro/intro) - [gRPC protocol](https://developer.tbank.ru/invest/intro/developer/protocols/grpc/) - [Limits](https://developer.tbank.ru/invest/intro/intro/limits) - [UsersService](https://developer.tbank.ru/invest/api/users-service) - [OperationsService](https://developer.tbank.ru/invest/api/operations-service) - [JS SDK](https://developer.tbank.ru/invest/sdk/faq_js) - [Official proto contracts](https://opensource.tbank.ru/invest/invest-contracts) ## Цели - Показать реальные брокерские счета T-Bank и ИИС в отдельном разделе брокерских портфелей. - Показать текущие позиции по каждому счету: акции, облигации, другие бумаги, если они вернулись из API, и денежные остатки. - Показать историю операций по каждому счету: покупки, продажи, комиссии, налоги, купоны, дивиденды, пополнения, выводы, погашения, корректировки и другие типы операций, которые возвращает T-Bank. - Не создавать лишнюю нагрузку на T-Bank: использовать ограничение частоты запросов на бэкенде, короткоживущий кеш чтения и устойчивую локальную синхронизацию операций. - Изолировать фронтенд от T-Bank. Бэкенд остается единственным клиентом внешнего API. - Опубликовать человекочитаемую архитектурную и пользовательскую документацию в `apps/docs`. ## Не цели - В этой фазе нет торговли, выставления заявок, переводов или пополнения счетов. - В этой фазе нет Инвесткопилки, ЦФА-счета, дебетового счета, накопительного счета или счета фонда денежного рынка. - В этой фазе нет управления T-Bank токенами на уровне пользователя. - В первой версии нет постоянно запущенных stream workers. - Нет собственного движка расчета налогов сверх отображения налоговых операций, которые предоставляет брокер. ## Область счетов Используем `UsersService/GetAccounts` и оставляем только: - `ACCOUNT_TYPE_TINKOFF` - `ACCOUNT_TYPE_TINKOFF_IIS` Также оставляем только открытые счета: - `ACCOUNT_STATUS_OPEN` Исключенные типы счетов: - `ACCOUNT_TYPE_INVEST_BOX` - `ACCOUNT_TYPE_INVEST_FUND` - `ACCOUNT_TYPE_DEBIT` - `ACCOUNT_TYPE_SAVING` - `ACCOUNT_TYPE_DFA` - `ACCOUNT_TYPE_UNSPECIFIED` ## Решение по протоколу Используем тонкую gRPC-интеграцию на стороне бэкенда вместо REST-прокси или жесткой зависимости от официального JS SDK. ### Почему gRPC - Это основной протокол T-Bank Invest API. - Он дает прямой доступ к unary API и будущим stream API. - Он оставляет метаданные запроса, `x-tracking-id` и метаданные лимитов видимыми на границе транспорта. - Он соответствует правилу MoexVibe: бэкенд является единственным клиентом внешних данных. ### Почему не REST первым REST полезен для ручной отладки и Swagger-примеров, но это прокси поверх той же поверхности сервисов. Если строиться на REST, появится лишний слой перевода, а будущая поддержка stream API потребует отдельного дизайна. ### Почему не SDK первым Официальный JS SDK существует и может быть полезен как справочник. Текущие npm-метаданные `@tinkoff/invest-js` показывают, что SDK уже зависит от gRPC-библиотек, включая `@grpc/grpc-js`, `@grpc/proto-loader`, `nice-grpc` и `protobufjs`. Прямое использование SDK скроет транспортные детали, которыми MoexVibe должен владеть сам: ограничение частоты запросов, сбор метаданных, тестовые двойники, обновление сгенерированных типов и будущий источник токенов. Граница модуля при этом должна позволять заменить внутренний транспортный адаптер на вызовы SDK, если позже это станет очевидно дешевле. ## Архитектура бэкенда Создать новый feature-модуль: ```text apps/backend/src/modules/tbank/ ├── tbank.module.ts ├── tbank.config.ts ├── tbank.controller.ts ├── services/ │ ├── tbank-client.service.ts │ ├── broker-accounts.service.ts │ ├── broker-portfolio.service.ts │ ├── broker-operations.service.ts │ └── broker-instruments.service.ts ├── dto/ │ ├── broker-account-response.dto.ts │ ├── broker-portfolio-response.dto.ts │ ├── broker-operation-query.dto.ts │ └── broker-operation-response.dto.ts └── mappers/ ├── money.mapper.ts ├── account.mapper.ts ├── portfolio.mapper.ts └── operation.mapper.ts ``` `TBankClientService` отвечает за: - создание gRPC channel; - метаданные `Authorization`; - опциональные метаданные `x-app-name`, например `ksv741.moex-vibe`; - таймаут запроса; - повторные попытки для временных gRPC-сбоев; - ограничение частоты запросов на уровне сервисов; - сбор `x-tracking-id` и метаданных лимитов для логов и ответов об ошибках. Доменные сервисы зависят от `TBankClientService`, а не напрямую от сгенерированных gRPC-клиентов. Это оставляет будущий переход от одного серверного токена к providers токенов на уровне пользователя локальным для клиентского слоя. ## Публичный API бэкенда Все endpoints требуют существующую JWT-аутентификацию. В первой версии любой аутентифицированный пользователь MoexVibe технически сможет прочитать одни и те же брокерские данные серверного токена. До реализации хранилища токенов на уровне пользователя deployment должен считаться single-user/admin-only. | Endpoint | Метод | Описание | | --- | --- | --- | | `/api/v1/broker/accounts` | GET | Список открытых брокерских счетов T-Bank и ИИС. | | `/api/v1/broker/accounts/:accountId/portfolio` | GET | Текущий портфель счета, позиции, итоги, деньги. | | `/api/v1/broker/accounts/:accountId/operations` | GET | История операций с пагинацией и фильтрами. | Параметры query для операций: | Параметр | Тип | По умолчанию | Примечания | | --- | --- | --- | --- | | `from` | ISO datetime | начало текущего года | UTC в запросе к T-Bank. | | `to` | ISO datetime | сейчас | UTC в запросе к T-Bank. | | `cursor` | string | отсутствует | Передается в `GetOperationsByCursor`. | | `limit` | integer | 100 | Ограничить до `1..1000`. | | `instrumentId` | string | отсутствует | Поддерживает figi, instrument UID или `ticker_classCode`. | | `operationTypes` | string list | отсутствует | Опциональные имена enum типов операций T-Bank. | | `state` | enum | `OPERATION_STATE_EXECUTED` | По умолчанию исполненные операции для учетных представлений портфеля. | ## Модель ответа Используем JSON DTO, удобные для фронтенда, а не сырые proto-объекты. Денежные значения нормализуются из T-Bank `MoneyValue { currency, units, nano }` в: ```typescript type BrokerMoney = { currency: string; units: string; nano: number; value: number; }; ``` `units` остается строкой, чтобы сохранить исходное int64-значение. `value` — удобное decimal-число для отображения и графиков. Финансовые расчеты, которым нужна точная арифметика, должны использовать decimal helpers, а не арифметику с плавающей точкой. Форма ответа портфеля: ```typescript type BrokerPortfolio = { account: BrokerAccount; totals: { shares: BrokerMoney | null; bonds: BrokerMoney | null; etf: BrokerMoney | null; currencies: BrokerMoney | null; futures: BrokerMoney | null; options: BrokerMoney | null; structuredProducts: BrokerMoney | null; dfa: BrokerMoney | null; portfolio: BrokerMoney | null; }; yields: { expectedPercent: number | null; daily: BrokerMoney | null; dailyPercent: number | null; }; cash: BrokerMoney[]; blockedCash: BrokerMoney[]; positions: BrokerPosition[]; asOf: string; }; ``` Форма ответа операций: ```typescript type BrokerOperationsPage = { accountId: string; items: BrokerOperation[]; nextCursor: string | null; hasNext: boolean; asOf: string; }; ``` Каждая операция хранит исходный enum T-Bank в поле `type`, а также отображаемую категорию MoexVibe: | Категория | Примеры операций T-Bank | | --- | --- | | `trade` | `OPERATION_TYPE_BUY`, `OPERATION_TYPE_SELL`, маржинальные/поставочные варианты | | `income` | `OPERATION_TYPE_DIVIDEND`, `OPERATION_TYPE_COUPON`, погашения, overnight income | | `tax` | `OPERATION_TYPE_TAX`, налог на дивиденды, налог на облигации, прогрессивные налоги | | `fee` | брокерская комиссия, сервисная комиссия, маржинальная комиссия, success fee, прочие комиссии | | `transfer` | пополнение, вывод, перевод бумаг, SWIFT/acquiring/multi transfers | | `other` | unspecified и еще не категоризованные типы операций T-Bank | Нельзя отбрасывать неизвестные типы операций. Их нужно хранить и показывать как `other` с исходным enum-значением. ## Стратегия синхронизации операций Фаза 1 может читать операции напрямую из T-Bank с кешированием. Фаза 2 должна сохранять нормализованные операции локально, потому что история операций — это audit trail, и она не должна зависеть от повторного обхода одних и тех же удаленных страниц. Дизайн устойчивой синхронизации: - Хранить одну строку на каждый элемент операции T-Bank с ключом `(accountId, cursor)`, если cursor присутствует. - Также хранить `operationId`, `parentOperationId`, `date`, `type`, `state`, `instrumentUid`, `figi`, `ticker`, `classCode`, `payment`, `price`, `commission`, `yield`, `accruedInt`, `quantity`, `quantityDone`, `raw`. - Хранить состояние синхронизации по каждому счету: последний успешный диапазон, последний cursor, timestamp последней синхронизации. - Делать backfill исторических данных окнами по датам, например по одному календарному году за запуск. - Обновлять скользящее недавнее окно, например последние 3 дня, потому что operation IDs и parent IDs могут меняться согласно комментариям в proto. - Сохранять сырой JSON payload операции для аудита/отладки и отдавать наружу нормализованные DTO. ## Стратегия кеширования Использовать существующий backend `CacheService` для короткоживущих чтений и добавить key prefixes для T-Bank. Frontend TanStack Query должен использовать совпадающие или более короткие stale times. | Данные | TTL бэкенда | Причина | | --- | --- | --- | | Счета | 1 час | Список счетов меняется редко. | | Итоги портфеля и позиции | 30-60 секунд | Текущий пользовательский вид должен ощущаться свежим, но не спамить T-Bank. | | Деньги/лимиты вывода | 30-60 секунд | Такая же свежесть, как у позиций. | | Метаданные инструмента | 24 часа | Название инструмента, лот, ISIN, UID стабильны. | | Страница операций, недавнее окно | 60-300 секунд | Полезно до появления устойчивой синхронизации. | | Исторические страницы операций | 24 часа или только БД | История в основном неизменна вне недавнего окна корректировок. | Добавить переменные окружения: | Переменная | Значение по умолчанию | Описание | | --- | --- | --- | | `T_BANK_TOKEN` | нет | Серверный T-Bank Invest token. Обязателен, когда интеграция включена. | | `T_BANK_BASE_URL` | `invest-public-api.tbank.ru:443` | gRPC endpoint. | | `T_BANK_APP_NAME` | `ksv741.moex-vibe` | Опциональная gRPC metadata. | | `T_BANK_RATE_LIMIT_PER_SECOND` | `5` | Консервативный локальный limiter для всех вызовов T-Bank. | | `CACHE_TBANK_ACCOUNTS_TTL` | `3600` | TTL кеша счетов. | | `CACHE_TBANK_PORTFOLIO_TTL` | `60` | TTL кеша портфеля и позиций. | | `CACHE_TBANK_OPERATIONS_TTL` | `300` | TTL кеша страницы недавних операций. | | `CACHE_TBANK_INSTRUMENT_TTL` | `86400` | TTL кеша метаданных инструмента. | Rate limiter должен оставаться ниже документированных публичных лимитов. При 429 или исчерпанных метаданных лимита нужно отступать до reset, если метаданные доступны. ## Обработка ошибок - Отсутствует `T_BANK_TOKEN`: вернуть понятную application error в стиле 503 и пометить интеграцию недоступной; в development не ронять весь бэкенд. - Ошибка авторизации от T-Bank: вернуть 502/503 с общим сообщением; никогда не отдавать token или полные метаданные наружу. - Счет не найден или исключен по type/status: вернуть 404 из endpoints MoexVibe. - T-Bank 429: вернуть 429 или 503 с retry metadata, если она доступна. - Временные gRPC-ошибки T-Bank: повторные попытки с небольшим exponential backoff, затем типизированная upstream error с `trackingId`, если он присутствует. - Неизвестные enum values при mapping: сохранить исходное значение и категоризовать как `other`. ## Безопасность - `T_BANK_TOKEN` нельзя логировать, возвращать в API responses или коммитить. - Логирование gRPC metadata должно редактировать `Authorization`. - Первый deployment по дизайну single-user. Перед включением доступа для нескольких пользователей MoexVibe нужно добавить encrypted token storage на уровне пользователя и authorization rules, которые связывают каждый брокерский счет с владельцем. - Интеграция только для чтения. В первый модуль нельзя включать clients для orders, stop-orders, transfers или pay-in. ## Вид продукта на фронтенде Добавить раздел брокерских портфелей отдельно от вручную управляемых виртуальных портфелей: - Страница списка: карточки счетов для брокерских счетов и ИИС, с полной стоимостью, деньгами, дневным изменением и временем последнего обновления. - Страница счета: вкладки `Позиции`, `Операции` и позже `Аналитика`. - Таблица позиций: название инструмента, тикер, тип, количество, текущая цена, текущая стоимость, ожидаемая доходность, дневная доходность, заблокированное количество. - Секция денег: доступные и заблокированные деньги по валютам. - Таблица операций: дата, type/category, инструмент, количество, платеж, комиссия, признаки налога/дохода, статус, раскрываемые детали сделки. Не смешивать реальные брокерские счета с существующей ручной моделью `Portfolio`. Держать их отдельно в UI и backend API. Позже MoexVibe сможет добавить представления сравнения или import flows из брокерских операций в виртуальные портфели. ## Документация Во время реализации опубликовать устойчивую документацию в `apps/docs`: - `apps/docs/docs/backend/tbank-invest.md`: архитектура модуля, методы API, переменные окружения, обработка ошибок и область только для чтения. - Обновить `apps/docs/docs/backend/modules.md`: добавить `TBankInvestModule`. - Обновить `apps/docs/docs/backend/configuration.md`: задокументировать `T_BANK_*` и переменные TTL кеша. - Обновить `apps/docs/docs/backend/caching.md`: добавить стратегию кеширования для T-Bank. - Обновить `apps/docs/docs/backend/portfolio.md`: разделить ручные портфели и брокерские портфели. - Добавить ADR `apps/docs/docs/adr/ADR-011-tbank-invest-grpc.md`: зафиксировать решение gRPC over REST/SDK. - Обновить `apps/docs/sidebars.ts`, чтобы включить новую страницу бэкенда и ADR. ## Критерии приемки - Бэкенд показывает только открытые счета `ACCOUNT_TYPE_TINKOFF` и `ACCOUNT_TYPE_TINKOFF_IIS`. - Бэкенд отдает текущий портфель счета с позициями и деньгами, не раскрывая сырые данные токена. - Бэкенд отдает историю операций с пагинацией и включает категории buy, sell, tax, fee, coupon, dividend, deposit, withdrawal и unknown operations. - Вызовы T-Bank используют консервативный rate limiter и стратегию кеширования. - Отсутствующий или некорректный token дает понятную ошибку integration-unavailable. - Опубликованная документация в `apps/docs` описывает архитектуру, конфигурацию, кеширование и решение по gRPC. - Тесты покрывают фильтрацию счетов, mapping денег, категоризацию операций, валидацию query, cache keys и mapping upstream-ошибок. ## Этапы реализации 1. Базовая gRPC-инфраструктура бэкенда и DTO mappers. 2. Endpoints только для чтения для счетов и портфеля. 3. Endpoint истории операций с курсорной пагинацией. 4. Список брокерских счетов и detail pages счета на фронтенде. 5. Устойчивая синхронизация операций в базе данных. 6. Опциональные stream workers для near-real-time refresh после стабилизации unary sync.