26 KiB
Проектирование брокерских портфелей 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-метаданные:
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
- gRPC protocol
- Limits
- UsersService
- OperationsService
- JS SDK
- Official proto contracts
Цели
- Показать реальные брокерские счета T-Bank и ИИС в отдельном разделе брокерских портфелей.
- Показать текущие позиции по каждому счету: акции, облигации, другие бумаги, если они вернулись из API, и денежные остатки.
- Показать историю операций по каждому счету: покупки, продажи, комиссии, налоги, купоны, дивиденды, пополнения, выводы, погашения, корректировки и другие типы операций, которые возвращает T-Bank.
- Не создавать лишнюю нагрузку на T-Bank: использовать ограничение частоты запросов на бэкенде, короткоживущий кеш чтения и устойчивую локальную синхронизацию операций.
- Изолировать фронтенд от T-Bank. Бэкенд остается единственным клиентом внешнего API.
- Опубликовать человекочитаемую архитектурную и пользовательскую документацию в
apps/docs.
Не цели
- В этой фазе нет торговли, выставления заявок, переводов или пополнения счетов.
- В этой фазе нет Инвесткопилки, ЦФА-счета, дебетового счета, накопительного счета или счета фонда денежного рынка.
- В этой фазе нет управления T-Bank токенами на уровне пользователя.
- В первой версии нет постоянно запущенных stream workers.
- Нет собственного движка расчета налогов сверх отображения налоговых операций, которые предоставляет брокер.
Область счетов
Используем UsersService/GetAccounts и оставляем только:
ACCOUNT_TYPE_TINKOFFACCOUNT_TYPE_TINKOFF_IIS
Также оставляем только открытые счета:
ACCOUNT_STATUS_OPEN
Исключенные типы счетов:
ACCOUNT_TYPE_INVEST_BOXACCOUNT_TYPE_INVEST_FUNDACCOUNT_TYPE_DEBITACCOUNT_TYPE_SAVINGACCOUNT_TYPE_DFAACCOUNT_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-модуль:
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 } в:
type BrokerMoney = {
currency: string;
units: string;
nano: number;
value: number;
};
units остается строкой, чтобы сохранить исходное int64-значение. value — удобное decimal-число
для отображения и графиков. Финансовые расчеты, которым нужна точная арифметика, должны использовать
decimal helpers, а не арифметику с плавающей точкой.
Форма ответа портфеля:
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;
};
Форма ответа операций:
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-ошибок.
Этапы реализации
- Базовая gRPC-инфраструктура бэкенда и DTO mappers.
- Endpoints только для чтения для счетов и портфеля.
- Endpoint истории операций с курсорной пагинацией.
- Список брокерских счетов и detail pages счета на фронтенде.
- Устойчивая синхронизация операций в базе данных.
- Опциональные stream workers для near-real-time refresh после стабилизации unary sync.