Анализируем портфель с помощью python и T-Bank invest API

Анализируем портфель с помощью 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 доступен по ссылке.

Спасибо всем за внимание!


Report Page