395 lines
26 KiB
Markdown
395 lines
26 KiB
Markdown
# Проектирование брокерских портфелей 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_TOKEN>
|
||
```
|
||
|
||
Нужные официальные сервисы и методы:
|
||
|
||
| Потребность | Метод 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.
|