Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Программирование

Вдохновителями разработки сервиса статистики для Тинькофф Инвестиций стали:

О чем речь пойдет?

  • Только прикладная часть про разработку.
  • Реальные знания и опыт, который очень важны в работе с финансовыми инструментами.
  • Обзор проблем с которыми предстоит работать

Итак, я хочу посчитать статистику по сделкам и сделать это в удобном для себя виде. 

Разработка сервиса статистики по шагам: 

  1. Подключение к Tinkoff Invest API
  2. Отрисовка данных из Tinkoff Invest API в браузере
  3. Получение брокерских отчётов и операций
  4. Подсчёт и вывод интересующей информации
  5. Выводы и планы на будущее

Подключение к Tinkoff Invest API

Для подключения к API можно брать любой sdk из документации https://github.com/Tinkoff/investAPI#sdk. Или npm пакет `tinkoff-sdk-grpc-js`. Важно, чтобы пакет был обновлён до последней версии со стороны разработчиков.

Устанавливаем

npm i tinkoff-sdk-grpc-js

Проверяем

const { createSdk } = require(‘tinkoff-sdk-grpc-js’);

 

// Токен, который можно получить вот так 

const TOKEN = ‘YOURAPI’;

 

// Имя приложения, по которому вас смогут найти в логах ТКС.

const appName = ‘tcsstat’;

 

const sdk = createSdk(TOKEN, appName);

(async () => {

    console.log(await sdk.users.getAccounts());

})();

Результат: в консоль будет выведен список ваших счетов. Например, такой

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Разберём нюансы:

  • В списке счетов есть “Инвесткопилка”, с которой по API нельзя работать
  • Обратите внимание, что поля приходят в camelCase, в то время как в документации эти поля представлены в under_score. 
  • Так будет везде, поэтому нельзя просто взять и скопировать поле из документации.

Полезное:

  • Данный код вы можете найти в ветке проекта

https://github.com/pskucherov/tcsstat/tree/step1

https://github.com/pskucherov/tcsstat/compare/step1 

 

Отрисовка данных из Tinkoff Invest API в браузере

Я взял next.js и socket.io. Это не является настойчивой рекомендацией, выбирайте на свое усмотрение. 

npx create-next-app@latest

npm i socket.io socket.io-client

Сразу переходим к шагу дружбы next+socket+investapi, а все детали смотрите в разделе Полезное этого шага. 

Опишу детали: 

  • На стороне nodejs (сервера) есть файл pages/api/investapi.js. Именно там создаём сервер socket.io и подключаемся к investapi.
  • На стороне браузера (клиента) подключаемся к серверу по сокету и запрашиваем данные про счета у брокера. 
  • Данные от брокера получаем на серврере, после отправляем на клиент. При получении их на клиенте они выводятся в браузере. 

Результат:  в консоли браузера мы можем увидеть информацию о счетах.

То есть в  прошлом шаге мы информацию о счетах видели в консоли сервера (nodejs), в текущем шаге мы эту информацию передали на клиент (браузер).

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Теперь сделаем так, чтобы выбрать счёт можно было из браузера, а если нет токена то ошибка отправлялась в консоль. Работа простая и ничего нового, поэтому привожу только ссылки на коммиты

  1. https://github.com/pskucherov/tcsstat/commit/7e1ac57061e5e971588479015b06d8814d6609a9
  2. https://github.com/pskucherov/tcsstat/commit/b28ac973a57494f5232589b4cb6b9fb13b8af759 

Полезное:

  • Как подружить next и socket подробно описано здесь
  • Код дружбы next+socket+investapi:

https://github.com/pskucherov/tcsstat/commit/a443a4ac1bb4f0aa898f638128755fe7391ee381

Для кого сложно вышеизложенное, то остаёмся на данной ступени и разбираемся с кодом. Если есть вопросы — задавайте.

https://github.com/pskucherov/tcsstat/tree/step2

https://github.com/pskucherov/tcsstat/compare/step1…step2

Получение брокерских отчётов и операций

Для получения брокерских отчётов и операций существует три метода

  1. GetBrokerReport
  2. GetDividendsForeignIssuer
  3. GetOperationsByCursor

С самого старта важно знать: 

  • Брокерский отчёт формируется в режиме T-3, т.е. там отображаются сделки после фактического их исполнения. 
  • Соответственно, если вы запросите этот отчёт за последние два дня, то он будет готов через три дня. 
  • Чтобы получить сделки за последние дни используем метод для получения операций, но помним что их id и содержимое могут измениться после формирования брокерского отчёта.

GetBrokerReport

Чтобы получить брокерский отчёт нужно взять id счёта, дату начала и дату конца отчёта, но не более 31 дня. Отправляем запрос для генерации отчёта в API в generate_broker_report_request, в ответ получить taskId. После чего по этому taskId получаем данные из get_broker_report_response.

Так гласит документация, в реальности есть нюансы. Следим за руками:
  • Вам нужно сохранить TaskID навсегда именно для этих дат. 
  • Так как если вы его потеряете, то за запрошенные даты отчёт сначала будет приходить в ответе на запрос генерации, 
  • А потом и вовсе перестанет приходить.

Приступаем к написанию кода

Метод получения даты c учётом вычитания от текущей даты

const getDateSubDay = (subDay = 5, start = true) => {

    const date = new Date();

    date.setUTCDate(date.getUTCDate() — subDay);

 

    if (start) {

        date.setUTCHours(0, 0, 0, 0);

    } else {

        date.setUTCHours(23, 59, 59, 999);

    }

 

    return date;

};

Запрос генерации отчёта 

const brokerReport = await (sdk.operations.getBrokerReport)({

        generateBrokerReportRequest: {

            accountId,

            from,

            to,

        },

});

Результат:

  • По итогу первого выполнения команды получаем taskId. 
  • Отчёт начинает генерироваться на стороне брокера. Когда он будет готов неизвестно, ждём и периодически дёргаем taskId в ожидании отчёта.
  • Почему? Потому что если отчёт не готов, то кидает ошибку. Если отчёт не готов на стороне брокера, то это ошибка у вас в коде. Обрабатывайте, пожалуйста: 30058|INVALID_ARGUMENT|task not completed yet, please try again later

Код ожидания и получения отчёта выглядит примерно так.

const timer = async time => {

    return new Promise(resolve => setTimeout(resolve, time));

}

 

const getBrokerResponseByTaskId = async (taskId, page = 0) => {

    try {

        return await (sdk.operations.getBrokerReport)({

            getBrokerReportRequest: {

                taskId,

                page,

            },

        });

    } catch (e) {

        console.log(‘wait’, e);

        await timer(10000);

        return await getBrokerResponseByTaskId(taskId, page);

    }

};

Дальше происходит та самая магия. Мы останавливаем наш скрипт, запускаем снова, taskId у нас нет. Выполняем код с запросом taskId, но получаем уже не taskId, а сразу отчёт. Магия! И всё бы было хорошо, если бы так было всегда. Но через месяц данных не будет вовсе.

Полезное:

  • Немного теории изложено здесь и здесь.
  • Собираем код воедино, черновик будет выглядеть примерно так.

https://github.com/pskucherov/tcsstat/tree/step3.1

https://github.com/pskucherov/tcsstat/compare/step3.1

  • Если кто-то с этим столкнётся, то добро пожаловать в ишью. После того как починят эта магия потеряет свою силу и будет как-то иначе. Но на текущий момент (21.03.2023) работает именно так.

GetDividendsForeignIssuer

Кто-то может подумать, что метод выполнен аналогично предыдущему и можно использовать единый метод, в котором только менять название операций. А вот и не угадали! 

Нейминг там очень сильно отличается и в методах, и в возвращаемой информации. А отсчёт страниц начинается то с 0, то с 1. Чтобы не запутаться во всём этом, проще написать два разных метода. Что странно, т.к. логика работы одинаковая.

Я долго плевался, когда пытался сделать один метод и было меньше кода. Примеров здесь не будет.

GetOperationsByCursor

Мой любимый метод из этой троицы. Хоть и не самый точный, но зато самый адекватный. Делаем запрос от начала создания счёта до максимально возможной даты (закрытия счёта или текущей). Получаем ответ, берём курсор и перезапрашиваем до тех пор, пока есть данные. 

И код получается более лаконичным, чем в примерах выше.

const timer = async time => {

    return new Promise(resolve => setTimeout(resolve, time));

}

 

const getOperationsByCursor = async (sdk, accountId, from, to, cursor = ») => {

    try {

        const reqData = {

            accountId,

            from,

            to,

            limit: 1000,

            state: sdk.OperationState.OPERATION_STATE_EXECUTED,

            withoutCommissions: false,

            withoutTrades: false,

            withoutOvernights: false,

            cursor,

        };

 

        return await sdk.operations.getOperationsByCursor(reqData);

    } catch (e) {

        await timer(60000);

        return await getOperationsByCursor(sdk, accountId, from, to, cursor = »);

    }

};

Черновик для запуска лежит здесь:

https://github.com/pskucherov/tcsstat/tree/step3.3

https://github.com/pskucherov/tcsstat/compare/step3.3

Теперь мы готовы добавить получение операций в наше приложение. Если делать правильно, то нужно получить брокерские отчёты за всё время существования счёта. А для недостающих данных, тех самых Т-3, дозагрузить из операций. Но это можно выделить в отдельную статью.

Из основных нюансов, с которыми предстоит столкнуться — это склеить операции и брокерский отчёт.

  •  Если вы сегодня получили брокерский отчёт и операции за требуемые даты, сложили это всё в БД, то проблем нет. 
  • Проблемы у вас будут завтра, когда вы получите следующую порцию данных из отчёта и операций и решите их синхронизировать с имеющейся БД. 
  • Очень много нюансов про несовпадающие или изменяющиеся id после обработки
  • Затем для внебиржевого рынка id не совпадают совершенно.
  •  А также нюансов по синхронизации инструментов, которые опять таки не совпадают, из-за особенностей API. Но это уже другая история.

Добавим получение информации об операциях в наше приложение.

Основным вопросом будет то, где будут обрабатываться и храниться данные.

  •  Если вы делаете для себя, будете с разных устройств потреблять одни и те же данные. То вам нужно обрабатывать и хранить данные на сервере.
  • Если у вас будет множество разных данных потреблять много разных пользователей, то нужно принимать решение, что важнее: скорость у пользователей или экономия железа на вашей стороне. Кто может позволить себе бесконечное количество железа, тот считает всё у себя на сервере и делает пользователям супер быстро, экономя пользователю ресурсы, например батарею и трафик, что на телефонах бывает очень важно.

В свою очередь, считать в браузере не самое оптимальное решение в принципе. Поэтому что не дорого, то считаем у себя на сервере. Остальное выносим на клиент.

Очень хочется взять и посчитать комиссию на сервере. Но тут приходит нюанс под названием “интерактивность”. Допустим, у вас тысячи операций и на их получение нужно пять минут.

Что будет в это время у пользователя? Спиннер? Прогресс? Инфа про то сколько было загружено?

Идеально использовать “активное ожидание”, когда пользователь в процессе уже мог что-то увидеть.

Вот так

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Результат:

  • Загружается страница
  • Запрашиваются все счета
  • После чего для всех счетов запрашиваются все операции с комиссиями за исполненные сделки. По мере получения данных они отрисовываются в браузере.

Чтобы не фильтровать каждый раз данные в событиях, для каждого счёта дёргаем своё событие. Вот так:

socket.emit(‘sdk:getOperationsCommissionResult_’ + accountId, {

                items: data?.items,

                inProgress: Boolean(nextCursor),

});

Черновик для запуска лежит здесь:

https://github.com/pskucherov/tcsstat/tree/step3

https://github.com/pskucherov/tcsstat/compare/step2…step3

Двигаемся дальше. Здорово, что вы дочитали до этой строки! 

Подсчёт и вывод интересующей информации

Зависит от того, кому какая информация нужна. Поэтому сразу рассказываю основные нюансы, с которыми вы столкнётесь.

Работа с ценами 

Все, кто работает с финансами, знают, что денежные операции нужно выполнять только с целыми числами.

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Из-за неточности значений после запятой и накапливающейся ошибки при большом количестве операций. Именно поэтому все цены представлены в следующем формате

MoneyValue

FieldTypeDescription
currencystringСтроковый ISO-код валюты
unitsint64Целая часть суммы, может быть отрицательным числом
nanoint32Дробная часть суммы, может быть отрицательным числом

Обрабатываем их отдельно, затем приводим к значению цены:

quotation.units + quotation.nano / 1e9

Стоимость фьючерсных контрактов

Цена фьючерсов представлена в пунктах, когда у вас валютный фьючерс, надо знать курс. И конечно же цену в пунктах и шаг цены.

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Когда будете считать профит от сделок это может выстрелить, т.к. если будете считать общую сумму перемножая прайс на количество. Здесь надо аккуратней.

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Пока так, как будет дальше – посмотрим. Это касается валютных фьючерсов, в других местах с этим всё ок.

Внебиржевой рынок

У этого рынка много особенностей, поэтому отдельно исследуем операции по нему

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Когда начнёте синхронизировать операции, то окажется что надо приводить figi / ticker к одному виду, чтобы правильно сопоставить инструмент. 

Когда начнёте синхронизировать это с брокерским отчётом, то окажется что в tradeID у одной и той же операции есть в начале буквы в операциях и их нет в брокерском отчёте. Поэтому их нельзя сравнить … кхм-кхм … сравнением! Я сопоставлял на время сделки, тикер и матчинг что один tradeId содержится в другом. Как правильно, не знаю.

Кто столкнётся с этим и кому это важно, приходите в ишью или заводите новый.

Математические операции над инструментами

Нельзя, не глядя, производить математические операции со всем списком. Чтобы не складывать тёплое с мягким, всегда проверяем валюту и обрабатываем только, если убедились, что валюта совпадает, а пункты переведены в нужную валюту.

Вооружившись знаниями про работу с банковскими числами посчитаем затраченную комиссию по каждому из счетов.Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Вот так:

https://github.com/pskucherov/tcsstat/tree/step4 

https://github.com/pskucherov/tcsstat/compare/step3…step4

 

Микросервис готов!

https://github.com/pskucherov/tcsstat

В качестве домашнего задания можете проверить работает ли сервис при медленном соединении, при обрыве соединений, при отключении интернета, при ошибках или закончившихся лимитах со стороны брокера. 

Выводы и планы на будущее

  • Узнали про базовые операции и работу с Invest API
  • Потраченное время ~ 10 часов
  • Уровень сложности ~ junior+ / low middle 

Если продолжать микросервис дорабатывать, то может получиться нечто вроде вот этого

https://opexbot.info

 
Это моя разработка, для тех кому лень разбираться, запускать и считать самостоятельно. Планирую добавить туда аналитику по просьбам использующих.

Если вам понравилась статья, то подписывайтесь на мой телеграм канал.

Разрабатываем микросервис используя Tinkoff Invest API для автоматизации работы с брокерскими отчётами и подсчёта комиссий.

Pavel
Оцените автора
Добавить комментарий

  1. Isakiiev

    Полезная статья. Не могу представить, сколько усилий автора потребовалось, чтобы все описать. Благодарю.

    Ответить