API Тиньков.Инвестиции. Первые шаги

Практически с первых дней я стал клиентом Тиньков.Инвестиции.

И с этого же момента меня терзают смутные сомнения — отражает ли личный кабинет объективную реальность?

Дело в том, что я покупаю ценные бумаги, номинированные в долларах, но в ЛК цены всех активов отображаются в долларах, а итоговая стоимость портфеля в рублях.

И мне непонятно, это доллар вырос или я такой результативный инвестор?

А как же комиссии, налоги и прочие дивиденды?

Вот бы взять все мои сделки и расписать по ФИФО, как в складском учете… А сверху положить полученные дивиденды, а потом вычесть налоги.

Вот тогда я и увижу понятный мне результат.

Оказалось, у Тинькова есть API, которое позволяет писать торговых роботов (мне это совсем не интересно), а также загружать данные по своему портфелю и операциям.

У этого API есть официальное описание, но мне не все было понятно, пришлось разбираться.
Результаты этих разборок представляю вашему вниманию.

Полезные ссылки:

Описание API
Еще описание

Получение токена и установка библиотеки


Перед началом работы нужно установить библиотеку и получить токен.

Установка библиотеки:

pip install -i https://test.pypi.org/simple/ --extra-index-url=https://pypi.org/simple/ tinkoff-invest-openapi-client


Цитирую официальную инструкцию по получению токена:

  1. Зайдите в свой аккаунт на tinkoff.ru
  2. Перейдите в раздел инвестиций
  3. Перейдите в настройки
  4. Функция «Подтверждение сделок кодом» должна быть отключена
  5. Выпустите токен OpenApi для биржи и Sandbox. Возможно система попросит вас авторизоваться еще раз, не беспокойтесь, это необходимо для подключения робота к торговой платформе.
  6. Скопируйте токен и сохраните, токен отображается только один раз, просмотреть его позже не получится, тем не менее вы можете выпускать неограниченное количество токенов.


На момент написания статью токен выдавался на странице www.tinkoff.ru/invest/settings, кнопка в нижней части страницы.

image

С токеном для песочницы у меня возникали ошибки, поэтому я начал экспериментировать с боевой версией. Чего и вам желаю (Осторожно: не купите-продайте случайно что-то лишнее).

Авторизация

from openapi_client import openapi

token = 'тут нужно вставить ваш токен'  
client = openapi.api_client(token)


Эти две строки делают все, что нам нужно.

Дальше работаем с переменной client.

Что у нас в портфеле


Получим содержимое нашего вашего портфеля:

pf = client.portfolio.portfolio_get()


Посмотрим основные данные первого элемента:

print('value:', pf.payload.positions[0].average_position_price.value)
print('currency:', pf.payload.positions[0].average_position_price.currency)
print('balance:', pf.payload.positions[0].balance)
print('figi:', pf.payload.positions[0].figi)
print('ticker:', pf.payload.positions[0].ticker)
print('name:', pf.payload.positions[0].name)


В моем случае это:

value: 45.98
currency: USD
balance: 21.0
figi: BBG000BWPXQ8
ticker: BTI
name: British American Tobacco


value — Цена бумаги
balance — Кличество бумаг в портфеле, value и currency — их денежное выражение.

figi — Financial Instrument Global Identifier (Финансовый Глобальный Идентификатор инструмента)
ticker — Тикер актива.

По этим данным мы можем узнать человекочитаемое название актива.

Для данного запроса нам это не нужно (см. поле name), но в других случаях пригодится.

Получаем название бумаги по FIFI и тикету

# Получение инструмента по FIGI
instr = client.market.market_search_by_figi_get('BBG000BWPXQ8') 
instr


Получаем:

{'payload': {'currency': 'USD',
             'figi': 'BBG000BWPXQ8',
             'isin': 'US1104481072',
             'lot': 1,
             'min_price_increment': 0.01,
             'name': 'British American Tobacco',
             'ticker': 'BTI',
             'type': 'Stock'},
 'status': 'Ok',
 'tracking_id': 'a1979917d2141916'}


Эта API-функция у меня работает как надо. Видим, что 'BBG000BWPXQ8' → 'British American Tobacco'.

А вот поиск названия актива по тикеру у меня не работает :(((

instr = client.market.market_search_by_ticker_get('BTI' ) 
print(instr)


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

Качаем справочник ценных бумаг


Впрочем, я решил этот вопрос кардинально. Скачал у Тинькова полный справочник торгуемых активов:

# Получение списка облигаций
bonds = client.market.market_bonds_get() 

# Получение списка ETF
etfs = client.market.market_etfs_get() 

# Получение списка акций
stocks = client.market.market_stocks_get() 

instr_list = bonds.payload.instruments + etfs.payload.instruments + stocks.payload.instruments

instr_list[:3]

получил

[{'currency': 'RUB',
  'figi': 'BBG00844BD08',
  'isin': 'RU000A0JU898',
  'lot': 1,
  'min_price_increment': 0.1,
  'name': 'МКБ выпуск 9',
  'ticker': 'RU000A0JU898'}, {'currency': 'RUB',
  'figi': 'BBG00R05JT04',
  'isin': 'RU000A1013Y3',
  'lot': 1,
  'min_price_increment': 0.1,
  'name': 'Черкизово выпуск\xa02',
  'ticker': 'RU000A1013Y3'}, {'currency': 'RUB',
  'figi': 'BBG00PNLY692',
  'isin': 'RU000A100DC4',
  'lot': 1,
  'min_price_increment': 0.1,
  'name': 'МСБ-Лизинг 002P выпуск 2',
  'ticker': 'RU000A100DC4'}]


Как видим, figi и name там есть. Для моих целей — более чем достаточно.

Получаем список операций


А вот самое интересное — получить список моих операций. В операции (в моем случае) попадают следующие действия:

  • PayIn — Пополнение брокерского счета
  • PayOut — Вывод денег
  • BuyCard — Покупка с карты
  • Sell — Продажа
  • BrokerCommission — Комиссия брокера
  • Dividend — Выплата дивидендов
  • Tax — Налоги
  • TaxDividend- Налоги c дивидендов
  • ServiceCommission — Комиссия за обслуживание


Код для выгрузки портфеля:

from datetime import datetime
from pytz import timezone

# Качаем все операции с 30 сентября 2016 (я один из первых клиентов Тиньков Инвестиции)
d1 = datetime(2016, 9, 30, 0, 0, 0, tzinfo=timezone('Europe/Moscow'))  # timezone нужно указывать. Иначе - ошибка
d2 = datetime.now(tz=timezone('Europe/Moscow'))  # По настоящее время
ops = client.operations.operations_get(_from=d1.isoformat(), to=d2.isoformat())

Посмотрим, что получилось. В моем случае, представляет интерес этот элемент

ops.payload.operations[217]

Вот что он собой являет

{'commission': {'currency': 'USD', 'value': -0.42},
 'currency': 'USD',
 'date': datetime.datetime(2018, 11, 7, 10, 55, 53, 648913, tzinfo=tzoffset(None, 10800)),
 'figi': 'BBG000PSKYX7',
 'id': '42281525510',
 'instrument_type': 'Stock',
 'is_margin_call': False,
 'operation_type': 'BuyCard',
 'payment': -141.05,
 'price': 141.05,
 'quantity': 4,
 'status': 'Done',
 'trades': [{'date': datetime.datetime(2018, 11, 7, 10, 55, 53, 648913, tzinfo=tzoffset(None, 10800)),
             'price': 141.05,
             'quantity': 1,
             'trade_id': '42636800'}]}


Нас интересуют поля:

  • date — дата сделки
  • figi — код актива
  • operation_type — тип операции
  • payment — сумма операции. У налогов или комиссий указана именно она. price при этом None
  • price — цена одной бумаги
  • quantity — плановое количество бумаг
  • trades — реальные биржевые сделки


Сразу возник вопрос — зачем нам какие-то trades, если есть price и quantity?

Все не так просто (план и факт)


Как я понял, в quantity указано то количество бумаг, которые я хотел купить. А то, что фактически куплено, лежит в trades[i].quantity.

Т.е. если хотите обратится к фактическим сделкам, нужно перебрать то, что лежит в trades.

В ряде случаев, там None — например, для налогов или вводов/выводов средств.

Чтобы получить настоящие цифры, нужно смотреть и в сделки, и в биржевые операции:

for op in ops.payload.operations: # Перебираем операции
    print(op.figi) # figi всегда берем из операции
    print(op.operation_type)   # и тип операции тоже
    if op.trades == None:      # Если биржевых сделок нет
        print('price:', op.price)       # Берем из операции цену бумаги
        print('payment:', op.payment)   # Сумму платежа
        print('quantity:', op.quantity) # И количество бумаг
    else:     
        for t in op.trades:                   # А если есть сделки - то перебираем их
            print('price:', t.price)          # И берем данные из них
            print('quantity:', t.quantity)
    print('--------------')

© Habrahabr.ru