Поддержать проект: https://yoomoney.ru/to/410015019068268
В прошлой статье я рассказал про опенсорс разработку — платформу TKSBrokerAPI — это Python API для работы с Tinkoff Open API через REST протокол. Также этим инструментом можно пользоваться из консоли или встраивать его в CI/CD-системы, для автоматизации рутинных операций на бирже, через брокера Тинькофф Инвестиции.
С помощью TKSBrokerAPI вы можете реализовать на языке Python любой торговый сценарий. В сегодняшней статье я хочу показать для разработчиков некоторые возможности этого инструмента, на примере абстрактного торгового сценария.
Неважно, какую основную систему принятия торговых решений о покупке или продаже вы используете. Это может быть технический анализ, нейросети, парсинг отчётов или слежение за сделками других трейдеров. Но всё равно вам потребуется выполнять базовые торговые операции: получать рыночные данные, выставлять лимитные и стоп-ордера, открывать и закрывать сделки по рынку. Модуль TKSBrokerAPI будет выступать как посредник между кодом с логикой торгов и сервисной инфраструктурой брокера, а также выполнять рутинные задачи от вашего имени в брокерском аккаунте.
Схема разработки с помощью TKSBrokerAPI очень простая:
- Вы придумываете гениальный торговый алгоритм.
- Записываете его пошагово в виде некоторого плана или торгового сценария.
- Автоматизируете сценарий на Python при помощи TKSBrokerAPI.
- TKSBrokerAPI берёт на себя всю работу с инфраструктурой брокера Тинькофф Инвестиции.
- Профит!
С чего начать
Проще всего установить TKSBrokerAPI через PyPI:
pip install tksbrokerapi
Либо скачать код проекта TKSBrokerAPI из гитхаб-репозитория.
Перед тем, как использовать API, вам нужно создать себе аккаунт у брокера Тинькофф Инвестиции. Для аутентификации через API вам также понадобятся токен и идентификатор счёта пользователя. Полная документация по всем доступным свойствам и методам TKSBrokerAPI находится по ссылке. Соответствие консольных ключей и методов также можно посмотреть в документации в разделе "Основные возможности".
Важное замечание: модуль TKSBrokerAPI не предназначен для высокочастотной (HFT) торговли, из-за системы динамического формирования лимитов для пользователей сервиса TINKOFF INVEST API (подробнее по ссылке). В среднем, это 50-300 запросов в секунду, в зависимости от их типа, что очень мало для требований к скоростям HFT (но есть несколько рекомендаций по ускорению исполнения поручений). Однако вы вполне можете использовать API для автоматизации своих интрадей, кратко-, средне- и долгосрочных торговых стратегий.
Пример реализации абстрактного сценария
Так как функциональность TKSBrokerAPI достаточно обширная, мне не хочется акцентировать внимание на конкретных торговых сценариях, а лишь указать некоторые возможности для их автоматизации. Давайте рассмотрим один сценарий, основанный на сравнении объёмов текущих покупок и продаж в биржевом стакане, и реализуем его при помощи API, без использования дополнительных методов технического анализа.
Действия будут следующие:
- запросить текущий портфель клиента и определить доступные для торговли средства;
- запросить стакан цен с глубиной 20 для выбранных инструментов, например, акции с тикерами `YNDX`, `IBM` и `GOOGLE`;
- если инструмент ранее ещё не был куплен, то проверить:
- если резерв денежных средств (свободный кеш) в валюте инструмента больше, чем 5% от общей стоимости всех инструментов в этой валюте, то проверить:
- если в стакане объёмы на покупку больше объёмов на продажу минимум на 10%, то купить 1 акцию по рынку и выставить тейк-профит как стоп-ордер на 3% выше текущей цены покупки со сроком действия 1 час;
- если инструмент имеется в списке открытых позиций, то проверить:
- если текущая цена уже выше средней цены позиции хотя бы на 2.5%, то выставить отложенный лимитный ордер на весь объём, но ещё чуть-чуть выше (на 0.1%) от текущей цены, чтобы позиция закрылась с профитом с большой вероятностью в течении текущей торговой сессии;
- после всех торговых операций напечатать в консоль текущее состояние портфеля пользователя.
Для понимания примера сохраните и запустите скрипт под спойлером ниже (или скачайте его по ссылке). Не забудьте перед этим подставить свой token и accountId в разделе инициализации в коде. Большая часть кода подробно прокомментирована и даны ссылки на соответствующие методы API.
Пример кода с торговым сценарием
# -*- coding: utf-8 -*-
# Author: Timur Gilmullin
# --- Секция инициализации: импорты, константы и переменные ------------------------------------------------------------
from datetime import datetime, timedelta
from dateutil.tz import tzlocal, tzutc
from math import ceil
from tksbrokerapi.TKSBrokerAPI import TinkoffBrokerServer, uLogger # основной модуль для выполнения торговых операций
uLogger.level = 10 # DEBUG (10) уровень логирования, рекомендованный по умолчанию для `TKSBrokerAPI.log`
uLogger.handlers[0].level = 20 # уровень логирования для вывода в консоль STDOUT, INFO (20) рекомендовано по умолчанию
start = datetime.now(tzutc())
uLogger.debug("=--=" * 20)
uLogger.debug("Trading scenario started at: [{}] UTC, it is [{}] local time".format(
start.strftime("%Y-%m-%d %H:%M:%S"),
start.astimezone(tzlocal()).strftime("%Y-%m-%d %H:%M:%S"),
))
# Установите здесь переменные и константы, необходимые для торговли по вашему алгоритму:
TICKERS_LIST_FOR_TRADING = ["YNDX", "IBM", "GOOGL"] # Вы можете задать список инструментов различным образом: перечислить их напрямую или задать как результат некоторой функции фильтрации или скринера
RESERVED_MONEY = 0.05 # Доля резервируемых средств (от 0 до 1), не участвующих в торгах, 0.05 (это 5%) по умолчанию
LOTS = 1 # Минимальное число лотов для покупки или продажи
TP_STOP_DIFF = 0.03 # 3% тейк-профит по умолчанию для стоп-ордеров
TP_LIMIT_DIFF = 0.025 # 2.5% тейк-профит по умолчанию для отложенных лимитных ордеров
TOLERANCE = 0.001 # Допустимое отклонение текущей рыночной цены от целевой цены установленных ордеров, 0.1% по умолчанию
DEPTH_OF_MARKET = 20 # Насколько глубоко запрашивать стакан цен для анализа текущих объёмов торгов, >= 1
VOLUME_DIFF = 0.1 # Достаточная разница в объёмах текущих предложений на покупку и продажу для открытия позиции, 10% по умолчанию
# Инициализация основного объекта трейдера, TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.__init__
trader = TinkoffBrokerServer(
token="", # Внимание! Установите строку с вашим токеном сюда или используйте переменную окружения `TKS_API_TOKEN`
accountId="", # Внимание! Установите строку с вашим accountId сюда или используйте переменную окружения `TKS_ACCOUNT_ID`
)
# --- Секция описания торгового сценария -------------------------------------------------------------------------------
for ticker in TICKERS_LIST_FOR_TRADING:
uLogger.info("--- Ticker [{}], data analysis...".format(ticker))
# - Шаг 1: запрос текущего портфеля клиента и определение доступных объёмов и валют для торговли
# Портфель пользователя. Это словарь с несколькими секциями: {"raw": {...}, "stat": {...}, "analytics": {...}}
portfolio = trader.Overview(showStatistics=False) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.Overview
uLogger.info("Total portfolio cost: {:.2f} rub; blocked: {:.2f} rub; changes: {}{:.2f} rub ({}{:.2f}%)".format(
portfolio["stat"]["portfolioCostRUB"],
portfolio["stat"]["blockedRUB"],
"+" if portfolio["stat"]["totalChangesRUB"] > 0 else "", portfolio["stat"]["totalChangesRUB"],
"+" if portfolio["stat"]["totalChangesPercentRUB"] > 0 else "", portfolio["stat"]["totalChangesPercentRUB"],
))
# Сколько денег в различных валютах доступно для торговли? Нужно посчитать (total - blocked).
funds = portfolio["stat"]["funds"] # Словарь, например: {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}, ...}
uLogger.info("Available funds free for trading: {}".format("; ".join(["{:.2f} {}".format(funds[currency]["free"], currency) for currency in funds.keys()])))
# - Шаг 2: запрос стакана цен для текущего инструмента
trader.ticker = ticker
trader.figi = "" # Мы не знаем FIGI для каждого тикера, поэтому указываем здесь пустую строку. В этом случае TKSBrokerAPI определит FIGI автоматически.
trader.depth = DEPTH_OF_MARKET
# Получаем цены брокера для текущего инструмента:
ordersBook = trader.GetCurrentPrices(showPrice=False) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.GetCurrentPrices
if not (ordersBook["buy"] and ordersBook["sell"]):
uLogger.warning("Not possible to trade an instrument with the ticker [{}]! Try again later.".format(trader.ticker))
else:
# - Шаг 3: если инструмент отсутствует в списке текущих открытых позиций пользователя, то проверяем:
# - если денежный резерв (свободные деньги) в валюте инструмента больше, чем 5% от общей стоимости
# всех инструментов в этой валюте, то проверяем:
# - если объёмы покупателей в стакане больше хотя бы на 10% чем объёмы продавцов, тогда покупаем 1 лот инструмента
# по рынку и размещаем тейк-профит как стоп-ордер на 3% выше, чем текущая цена покупки, с отменой ордера через 1 час;
# Проверяем, есть ли открытые позиции по текущему инструменту, заданному через `ticker`, в портфеле пользователя:
isInPortfolio = trader.IsInPortfolio(portfolio) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.IsInPortfolio
if not isInPortfolio:
uLogger.info("Ticker [{}]: no current open positions with that instrument, checking opens rules...".format(trader.ticker))
# Так как инструмента нет среди открытых позиций, то получаем данные по инструменту и его валюте у брокера:
rawIData = trader.SearchByTicker(requestPrice=False, showInfo=False, debug=False) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.SearchByTicker
iCurr = rawIData["currency"] # валюта текущего инструмента
# Получаем аналитику портфеля: распределение активов по валютам, стоимость ранее купленных активов и доступный свободный остаток:
distrByCurr = portfolio["analytics"]["distrByCurrencies"] # распределение активов по валютам, оценка стоимости в рублях
assetsCostInRuble = distrByCurr[iCurr]["cost"] # стоимость активов в валюте инструмента, пересчитанная в рубли
currencyFreeCostInRuble = funds[iCurr]["freeCostRUB"] # оценка свободных средств, пересчитанная в рублях, для валюты текущего инструмента
# Прежде чем совершить сделку, проверяем резервы и разницу объёмов спроса и предложения, в соответствии с заданными параметрами:
if currencyFreeCostInRuble / assetsCostInRuble >= RESERVED_MONEY:
sumSellers = sum([x["quantity"] for x in ordersBook["buy"]]) # текущий объём предложений продавцов в стакане (у продавцов можно купить)
sumBuyers = sum([x["quantity"] for x in ordersBook["sell"]]) # текущий объём предложений покупателей в стакане (покупателям можно продать)
if sumBuyers >= sumSellers * (1 + VOLUME_DIFF):
# Получаем текущую цену, вычисляем цену потенциального тейк-профита и срок действия для стоп-ордера:
currentPriceToBuy = ordersBook["buy"][0]["price"] # первая цена в списке ордеров продавцов и есть актуальная цена, по которой можно купить
target = currentPriceToBuy * (1 + TP_STOP_DIFF) # целевая цена для тейк-профита, без учёта шага изменения цены
targetStop = ceil(target / rawIData["step"]) * rawIData["step"] # реальная цена тейк-профита для размещения стоп-ордера, с учётом допустимого шага цены
localAliveTo = (datetime.now() + timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S") # текущее время + 1 час
uLogger.info("Opening BUY position... (Buyers volumes [{}] >= {} * sellers volumes [{}] and current price to buy: [{:.2f} {}])".format(
sumBuyers, 1 + VOLUME_DIFF, sumSellers, currentPriceToBuy, iCurr,
))
# Открываем BUY позицию по рынку и создаём стоп-ордер по желаемой цене тейк-профита:
trader.Buy(lots=LOTS, tp=targetStop, sl=0, expDate=localAliveTo) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.Buy
else:
uLogger.info("BUY position not opened, because buyers volumes [{}] < {} * sellers volumes [{}]".format(sumBuyers, 1 + VOLUME_DIFF, sumSellers))
else:
uLogger.info("BUY position not opened, because the reserves in [{}] will be less than {:.2f}% of free funds".format(iCurr, RESERVED_MONEY * 100))
else:
# - Шаг 4: если по инструменту уже была открыта позиция, то проверяем:
# - если текущая средняя цена позиции хотя бы на 2.5% выше, чем средняя цена покупки, то размещаем отложенный
# лимитный ордер на весь объём позиции по цене на 0.1% выше, чем текущая рыночная цена. Это нужно для того, чтобы позиция
# закрылась с профитом, с большой вероятностью в течение текущей торговой сессии.
uLogger.info("Ticker [{}]: there is an open position with that instrument, checking closure rules...".format(trader.ticker))
# Получаем информацию по инструменту из списка текущих открытых позиций в портфеле пользователя:
iData = trader.GetInstrumentFromPortfolio(portfolio) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.GetInstrumentFromPortfolio
# Вычисляем количество доступных лотов для продажи, среднюю цену позиции и текущую рыночную цену инструмента:
lotsToSell = iData["volume"] - iData["blocked"] # не заблокированные лоты текущего инструмента, доступные для торговли
averagePrice = iData["average"] # средняя цена позиции
curPriceToSell = ordersBook["sell"][0]["price"] # первая цена в списке ордеров покупателей и есть актуальная цена, по которой можно продать инструмент
# Вычисляем цену с упреждением, по которой можно закрыть позицию, не дожидаясь строгого исполнения по цене тейк-профита:
curProfit = (curPriceToSell - averagePrice) / averagePrice # доля изменения между текущей рыночной ценой и средней позицией по инструменту
target = curPriceToSell * (1 + TOLERANCE) # достаточная цена для продажи
targetLimit = ceil(target / iData["step"]) * iData["step"] # целевая цена + упреждение, для размещения отложенного лимитного-ордера
# Проверяем на достаточную разницу в цене для профита:
if curProfit >= TP_LIMIT_DIFF:
uLogger.info("The current price is [{:.2f} {}], average price is [{:.2f} {}], so profit {:.2f}% more than {:.2f}%. Opening SELL pending limit order...".format(
curPriceToSell, iData["currency"], averagePrice, iData["currency"], curProfit * 100, TP_LIMIT_DIFF * 100,
))
# Открываем отложенный лимитный SELL ордер:
trader.SellLimit(lots=lotsToSell, targetPrice=targetLimit) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.SellLimit
else:
uLogger.info("SELL order not created, because the current price is [{:.2f} {}], average price is [{:.2f} {}], so profit {:.2f}% less than {:.2f}% target.".format(
curPriceToSell, iData["currency"], averagePrice, iData["currency"], curProfit * 100, TP_LIMIT_DIFF * 100,
))
# - Шаг 5: запрашиваем и отображаем изменения в портфеле пользователя после всех выполненных операций
uLogger.info("--- All trade operations finished. Let's show what we got in the user's portfolio after all trades.")
# Текущее состояние портфеля пользователя:
trader.Overview(showStatistics=True) # TKSBrokerAPI: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html#TinkoffBrokerServer.Overview
# --- Секция финализации торговых операций -----------------------------------------------------------------------------
finish = datetime.now(tzutc())
uLogger.debug("Trading scenario work duration: [{}]".format(finish - start))
uLogger.debug("Trading scenario finished: [{}] UTC, it is [{}] local time".format(
finish.strftime("%Y-%m-%d %H:%M:%S"),
finish.astimezone(tzlocal()).strftime("%Y-%m-%d %H:%M:%S"),
))
uLogger.debug("=--=" * 20)
Пусть вас не пугает кажущаяся сложность и объём кода. На самом деле, его большую часть занимают комментарии с примечаниями и логи, которые можно опустить в реальном проекте, а оставшийся алгоритм получится небольшим.
В следующих статьях я планирую рассказать про новый опенсорс проект, в котором будут храниться готовые торговые шаблоны и стратегии, а также аналитика на Python с использованием модуля TKSBrokerAPI. Их можно будет подключать и использовать как самостоятельно, так и в составе ваших собственных торговых сценариев.
