Анализируем портфель с помощью python и T-Bank invest API
Andrey KazakovПосле прочтения книги "Разумный инвестор" Бенджамина Грэма задался вопросом анализа своего инвестиционного портфеля. К сожалению, текущие приложения не позволяют детально анализировать свой портфель. Быстрый поиск по интернету показал, что у T-Bank есть хороший API к инвестициям, который позволит удовлетворить моим потребности. API хорошо задокументирован и найти по документации необходимые методы и структуры данных не составит труда. Документация по T-Invest API, репозиторий с реализацией на python.
Подключаем библиотеки
Устаналиваем необходимые зависимости:
pip install matplotlib pandas tinkoff-investmentskazakov/anaconda3/e
Подключаем необходимые библиотеки
import os from tinkoff.invest import Client from tinkoff.invest import InstrumentIdType, Quotation, Instrument import pandas as pd import matplotlib.pyplot as plt
Для работы с API нам понадобится токен доступа. Выпустить его можно по инструкции. Для своих нужд я выпустил Readonly token.
TOKEN = os.environ["INVEST_TOKEN"]
Получаем содержимое портфеля
Сначала получим список счетов используя клиент и токен доступа:
def get_accounts():
with Client(TOKEN) as client:
return client.users.get_accounts()
accounts_response = get_accounts()
У меня в приложении 3 счета - ивестокопилка, ИИС и брокерский счет. Мы получили все три счета:
accounts = accounts_response.accounts accounts
[Account(id='2********5', type=<AccountType.ACCOUNT_TYPE_INVEST_BOX: 3>, name='Инвесткопилка', status=<AccountStatus.ACCOUNT_STATUS_OPEN: 2>, ...), Account(id='2********8', type=<AccountType.ACCOUNT_TYPE_TINKOFF_IIS: 2>, name='ИИС', status=<AccountStatus.ACCOUNT_STATUS_OPEN: 2>, ...), Account(id='2********4', type=<AccountType.ACCOUNT_TYPE_TINKOFF: 1>, name='Брокерский счет', status=<AccountStatus.ACCOUNT_STATUS_OPEN: 2>, ...)]
В данный момент интересует ИИС, на нем есть некоторы средства. Получаем его id:
account_id = accounts[1].id
Получим список позиций портфеля:
with Client(TOKEN) as client:
portfolio = client.operations.get_portfolio(account_id=account_id)
portfolio_positions = portfolio.positions
Ниже представлено несколько вспомогательных функций, для обработки данных портфеля.
Маппинг отрасли и сектора по тикеру:
def get_branch(ticker: str, default_branch=None) -> str:
branch_by_ticker_dict = {
"LKOH": "Энергетические и минеральные ресурсы",
"RU000A10AHE5": None,
"NVTK": "Энергетические и минеральные ресурсы",
"PLZL": "Несырьевые полезные ископаемые",
"NLMK": "Несырьевые полезные ископаемые",
"IRAO": "Коммунальные услуги",
"HNFG": None,
"ALRS": "Несырьевые полезные ископаемые",
"GMKN": "Несырьевые полезные ископаемые",
"MAGN": "Несырьевые полезные ископаемые",
"T": "Финансы",
"PHOR": "Обрабатывающая промышленность",
"RUB000UTSTOM": None,
"SOFL": None,
"AQUA": "Обрабатывающая промышленность",
"RTKM": "Связь",
"X5": None,
"MDMG": None,
"TPAY": None,
"RU000A1085D5": None,
"TRNFP": "Производственно-технические услуги",
"TATN": "Энергетические и минеральные ресурсы",
"GAZP": "Энергетические и минеральные ресурсы",
"TMOS": None,
"SU26234RMFS3": None,
"RU000A0ZZGT5": None,
"SBER": "Финансы",
"TDIV": None,
}
return branch_by_ticker_dict[ticker] if ticker in branch_by_ticker_dict.keys() else default_branch
def get_sector(ticker: str, default_sector=None) -> str:
sector_by_ticker_dict = {
"LKOH": "Переработка и продажа нефти",
"RU000A10AHE5": None,
"NVTK": "Интегрированная нефтяная промышленность",
"PLZL": "Драгоценные металлы",
"NLMK": "Сталь",
"IRAO": "Электроэнергетика",
"HNFG": None,
"ALRS": "Прочие металлы и минералы",
"GMKN": "Прочие металлы и минералы",
"MAGN": "Сталь",
"T": "Региональные банки",
"PHOR": "Химическая промышленность: сельскохозяйственные химикаты",
"RUB000UTSTOM": None,
"SOFL": None,
"AQUA": "Сельскохозяйственные продукты и помол зерна",
"RTKM": "Ведущие телекоммуникационные компании",
"X5": None,
"MDMG": None,
"TPAY": None,
"RU000A1085D5": None,
"TRNFP": "Нефте- и газопроводы",
"TATN": "Переработка и продажа нефти",
"GAZP": "Интегрированная нефтяная промышленность",
"TMOS": None,
"SU26234RMFS3": None,
"RU000A0ZZGT5": None,
"SBER": "Региональные банки",
"TDIV": None,
}
return sector_by_ticker_dict[ticker] if ticker in sector_by_ticker_dict.keys() else default_sector
Преобразование из формата ответа в float:
def quotation_to_float(quotation: Quotation) -> float:
return float(quotation.units) + float(quotation.nano / 1000000000)
Загрузка инструмента, в зависимости от его типа:
def load_instrument(figi: str, instrument_type: str):
with Client(TOKEN) as client:
if instrument_type == "currency":
return client.instruments.currency_by(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=figi
).instrument
if instrument_type == "share":
return client.instruments.share_by(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=figi
).instrument
if instrument_type == "bond":
return client.instruments.bond_by(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=figi
).instrument
if instrument_type == "etf":
return client.instruments.etf_by(
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=figi
).instrument
Преобразуем данные в список dict:
account_data = []
for portfolio_position in portfolio_positions:
instruments_count = quotation_to_float(portfolio_position.quantity)
current_price = quotation_to_float(portfolio_position.current_price)
total_price = instruments_count * current_price
instrument = load_instrument(
portfolio_position.figi, portfolio_position.instrument_type
)
account_data.append(
{
# "figi": portfolio_position.figi,
# "isin": instrument.isin,
"ticker": instrument.ticker,
# "class_code": instrument.class_code,
"instrument_type": portfolio_position.instrument_type,
"name": instrument.name,
"instruments_count": instruments_count,
"current_price": current_price,
"total_price": total_price,
"sector": instrument.sector if hasattr(instrument, "sector") else "",
"branch": get_branch(instrument.ticker),
"ru_sector": get_sector(instrument.ticker)
}
)
Теперь загрузим данные в датафрейм для анализа:
df = pd.DataFrame(account_data) df
| | ticker | instrument_type | name | instruments_count | current_price | total_price | sector | branch | ru_sector | |---:|:-------------|:------------------|:-------------------------------------|--------------------:|----------------:|--------------:|:------------|:-------------------------------------|:----------------------------------------| | 0 | RTKM | share | Ростелеком | 20 | 69.18 | 1383.6 | telecom | Связь | Ведущие телекоммуникационные компании | | 1 | X5 | share | Корпоративный Центр Икс 5 | 1 | 3645.5 | 3645.5 | consumer | | | | 2 | MDMG | share | Мать и дитя | 2 | 1018.5 | 2037 | health_care | | |здесь | 3 | NLMK | share | НЛМК | 10 | 156.38 | 1563.8 | materials | Несырьевые полезные ископаемые | Сталь | | 4 | TPAY | etf | Пассивный доход | 25 | 96.08 | 2402 | other | | | | 5 | RU000A1085D5 | bond | Ростелеком 002P-14R | 1 | 964.5 | 964.5 | telecom | | | | 6 | TRNFP | share | Транснефть - привилегированные акции | 1 | 1203.4 | 1203.4 | energy | Производственно-технические услуги | Нефте- и газопроводы | | 7 | T | share | Т-Технологии | 1 | 3571 | 3571 | financial | Финансы | Региональные банки | | 8 | TATN | share | Татнефть | 2 | 698.2 | 1396.4 | energy | Энергетические и минеральные ресурсы | Переработка и продажа нефти | | 9 | GAZP | share | Газпром | 10 | 167.14 | 1671.4 | energy | Энергетические и минеральные ресурсы | Интегрированная нефтяная промышленность | | 10 | TMOS | etf | Крупнейшие компании РФ | 200 | 7.02 | 1404 | other | | | | 11 | RUB000UTSTOM | currency | Российский рубль | 67.7 | 1 | 67.7 | | | | | 12 | SU26234RMFS3 | bond | ОФЗ 26234 | 2 | 953.71 | 1907.42 | government | | | | 13 | RU000A0ZZGT5 | bond | РЖД 001Р выпуск 8 | 1 | 955.9 | 955.9 | industrials | | | | 14 | SBER | share | Сбер Банк | 10 | 323.28 | 3232.8 | financial | Финансы | Региональные банки | | 15 | TDIV | etf | Дивидендные акции | 233 | 10.68 | 2488.44 | other | | |
Здесь должна быть красивая табличка, но телеграф не хочет ее красиво выводить, можно посмотреть здесь.
Анализ портфеля
Построим распраделение финансов, в зависимости от типа инструмента - фонды, облигации и акции:
by_instruments = df.groupby("instrument_type")
by_instruments["total_price"].sum().plot(kind="bar", subplots=True)
array([<Axes: title={'center': 'total_price'}, xlabel='instrument_type'>], dtype=object)

Как видно из графика в моем портфеле явный перекос в сторону акций.
Теперь построим рапределение финансов внутри секторов в зависимости от сектора:
for name, grp in by_instruments:
ax = grp.groupby("sector")["total_price"].sum().plot(
kind="bar", title=name)
plt.show()




В акциях виден явный перекос в сторону финансового сектора. Посмотрим, какие акции входят в состав наибольших секторов - "consumer", "enegy" и "financial".
shares_grouped = by_instruments.get_group(name="share").groupby("sector")
shares_grouped.get_group(name="consumer").groupby("ticker")["total_price"].sum().plot(kind="bar", title=name)
plt.show()

shares_grouped.get_group(name="energy").groupby("ticker")["total_price"].sum().plot(kind="bar", title=name)
plt.show()

shares_grouped.get_group(name="financial").groupby("ticker")["total_price"].sum().plot(kind="bar", title=name)
plt.show()

В дальнейшем буду использовать данную информацию для ребаланса портфеля. Исходный код workbook доступен по ссылке.
Спасибо всем за внимание!