Вдохновителями разработки сервиса статистики для Тинькофф Инвестиций стали:
- статья на хабре «Что недоговаривают Тинькофф Инвестиции”
- анализ пожеланий пользователей платформы
- статья про подсчёт комиссий.
- О чем речь пойдет?
- Разработка сервиса статистики по шагам:
- Подключение к Tinkoff Invest API
- Отрисовка данных из Tinkoff Invest API в браузере
- Получение брокерских отчётов и операций
- GetBrokerReport
- Метод получения даты c учётом вычитания от текущей даты
- Запрос генерации отчёта
- Результат:
- GetDividendsForeignIssuer
- GetOperationsByCursor
- Подсчёт и вывод интересующей информации
- Работа с ценами
- Стоимость фьючерсных контрактов
- Внебиржевой рынок
- Математические операции над инструментами
- Микросервис готов!
- Выводы и планы на будущее
- https://opexbot.info
О чем речь пойдет?
- Только прикладная часть про разработку.
- Реальные знания и опыт, который очень важны в работе с финансовыми инструментами.
- Обзор проблем с которыми предстоит работать
Итак, я хочу посчитать статистику по сделкам и сделать это в удобном для себя виде.
Разработка сервиса статистики по шагам:
- Подключение к Tinkoff Invest API
- Отрисовка данных из Tinkoff Invest API в браузере
- Получение брокерских отчётов и операций
- Подсчёт и вывод интересующей информации
- Выводы и планы на будущее
Подключение к 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());
})();
Результат: в консоль будет выведен список ваших счетов. Например, такой
Разберём нюансы:
- В списке счетов есть “Инвесткопилка”, с которой по 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), в текущем шаге мы эту информацию передали на клиент (браузер).
Теперь сделаем так, чтобы выбрать счёт можно было из браузера, а если нет токена то ошибка отправлялась в консоль. Работа простая и ничего нового, поэтому привожу только ссылки на коммиты
- https://github.com/pskucherov/tcsstat/commit/7e1ac57061e5e971588479015b06d8814d6609a9
- 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
Получение брокерских отчётов и операций
Для получения брокерских отчётов и операций существует три метода
С самого старта важно знать:
- Брокерский отчёт формируется в режиме 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. Но это уже другая история.
Добавим получение информации об операциях в наше приложение.
Основным вопросом будет то, где будут обрабатываться и храниться данные.
- Если вы делаете для себя, будете с разных устройств потреблять одни и те же данные. То вам нужно обрабатывать и хранить данные на сервере.
- Если у вас будет множество разных данных потреблять много разных пользователей, то нужно принимать решение, что важнее: скорость у пользователей или экономия железа на вашей стороне. Кто может позволить себе бесконечное количество железа, тот считает всё у себя на сервере и делает пользователям супер быстро, экономя пользователю ресурсы, например батарею и трафик, что на телефонах бывает очень важно.
В свою очередь, считать в браузере не самое оптимальное решение в принципе. Поэтому что не дорого, то считаем у себя на сервере. Остальное выносим на клиент.
Очень хочется взять и посчитать комиссию на сервере. Но тут приходит нюанс под названием “интерактивность”. Допустим, у вас тысячи операций и на их получение нужно пять минут.
Что будет в это время у пользователя? Спиннер? Прогресс? Инфа про то сколько было загружено?
Идеально использовать “активное ожидание”, когда пользователь в процессе уже мог что-то увидеть.
Вот так
Результат:
- Загружается страница
- Запрашиваются все счета
- После чего для всех счетов запрашиваются все операции с комиссиями за исполненные сделки. По мере получения данных они отрисовываются в браузере.
Чтобы не фильтровать каждый раз данные в событиях, для каждого счёта дёргаем своё событие. Вот так:
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
Двигаемся дальше. Здорово, что вы дочитали до этой строки!
Подсчёт и вывод интересующей информации
Зависит от того, кому какая информация нужна. Поэтому сразу рассказываю основные нюансы, с которыми вы столкнётесь.
Работа с ценами
Все, кто работает с финансами, знают, что денежные операции нужно выполнять только с целыми числами.
Из-за неточности значений после запятой и накапливающейся ошибки при большом количестве операций. Именно поэтому все цены представлены в следующем формате
Field | Type | Description |
---|---|---|
currency | string | Строковый ISO-код валюты |
units | int64 | Целая часть суммы, может быть отрицательным числом |
nano | int32 | Дробная часть суммы, может быть отрицательным числом |
Обрабатываем их отдельно, затем приводим к значению цены:
quotation.units + quotation.nano / 1e9
Стоимость фьючерсных контрактов
Цена фьючерсов представлена в пунктах, когда у вас валютный фьючерс, надо знать курс. И конечно же цену в пунктах и шаг цены.
Когда будете считать профит от сделок это может выстрелить, т.к. если будете считать общую сумму перемножая прайс на количество. Здесь надо аккуратней.
Пока так, как будет дальше – посмотрим. Это касается валютных фьючерсов, в других местах с этим всё ок.
Внебиржевой рынок
У этого рынка много особенностей, поэтому отдельно исследуем операции по нему
Когда начнёте синхронизировать операции, то окажется что надо приводить figi / ticker к одному виду, чтобы правильно сопоставить инструмент.
Когда начнёте синхронизировать это с брокерским отчётом, то окажется что в tradeID у одной и той же операции есть в начале буквы в операциях и их нет в брокерском отчёте. Поэтому их нельзя сравнить … кхм-кхм … сравнением! Я сопоставлял на время сделки, тикер и матчинг что один tradeId содержится в другом. Как правильно, не знаю.
Кто столкнётся с этим и кому это важно, приходите в ишью или заводите новый.
Математические операции над инструментами
Нельзя, не глядя, производить математические операции со всем списком. Чтобы не складывать тёплое с мягким, всегда проверяем валюту и обрабатываем только, если убедились, что валюта совпадает, а пункты переведены в нужную валюту.
Вооружившись знаниями про работу с банковскими числами посчитаем затраченную комиссию по каждому из счетов.
Вот так:
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
Это моя разработка, для тех кому лень разбираться, запускать и считать самостоятельно. Планирую добавить туда аналитику по просьбам использующих.
Если вам понравилась статья, то подписывайтесь на мой телеграм канал.
Полезная статья. Не могу представить, сколько усилий автора потребовалось, чтобы все описать. Благодарю.