26 KiB
Raw Blame History

Проектирование брокерских портфелей 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-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-модуль:

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-ошибок.

Этапы реализации

  1. Базовая gRPC-инфраструктура бэкенда и DTO mappers.
  2. Endpoints только для чтения для счетов и портфеля.
  3. Endpoint истории операций с курсорной пагинацией.
  4. Список брокерских счетов и detail pages счета на фронтенде.
  5. Устойчивая синхронизация операций в базе данных.
  6. Опциональные stream workers для near-real-time refresh после стабилизации unary sync.