Готовимся резать лосей в портфеле

Nov 5, 2022 13:02 · 763 words · 4 minute read python tutorial

Скоро декабрь - самая пора резать лосей. Не тех, за которыми надо охотиться, а которые у нас в брокерском портфеле (loss - минусовые позиции). Тем более, что год выдался удивительным на события, и отрицательных позиций, уверен, много у каждого. Вот и я решил узнать что можно продать с минимальным убытком. Считать ручками как-то лень, так что весь код на github.

Немного теории

Наш налоговый кодекс допускает некоторую оптимизацию уплаты. Первая попавшаяся статья говорит нам, что можно провернуть много чего, но я пользуюсь только парой пунктов:

  • не платить налог с прибыли, если владеете бумагами больше 3х лет (срок владения)
  • зафиксированный убыток уменьшает налогооблагаемую базу от прибыльных сделок и снижает НДФЛ (фиксация убытка)

Продать акции, которые держу больше 3х лет, я всегда успею. Так что остановлюсь на оптимизации фиксации убытка. Но чтобы понимать что продавать хочется видеть график - когда, сколько и почём я покупал. К сожалению, мой брокер предоставляет такую справку только в виде плоского списка в html. Ни о какой визуализации речь не идёт. Более того, нужно организовать и партионный учёт, чтобы исключить уже проданные акции. Что ж, пришла пора звать тыжпрограммиста.

Решение задачи

Необходимо по данным в отчёте построить график покупок и актуальных остатков. Для начала спарсим всю информацию из html в удобный формат. Например, такой:

@dataclasses.dataclass
class Transaction:
    timestamp: datetime
    reason: bool  # True - покупка, False - продажа
    count: int
    price: Decimal


Portfolio = dict[str, list[Transaction]]

Не буду приводить код парсинга html, т.к. он будет отличаться от брокера к брокеру. Суть в том, что на выходе у нас должен получиться словарь с ключами - тикерами и значением из списка сделок.

Партионный учёт

Самое интересное - реализация партионного учёта. Эта такая штука в бухгалтерском деле, которая считает когда был продан товар из ранее купленной партии. На примере:

  • 29.08 купили 10 акций GAZP по 184 руб.
  • 30.08 купили ещё 10 акций по 200 руб.
  • 27.09 продали 5 акций по 214 руб., зафиксировав прибыль в (214-184)*5 = 150 руб.
  • 30.09 продали 10 акций по 235 руб., зафиксировав прибыль в [(235-184)*5 = 255] + [(235-200)*5 = 175] = 430 руб.

Т.е. в последней продаже мы продали 5 акций из первой партии и 5 из второй. Т.е. фактически у нас осталось 5 акций, купленных 30.08 по 200 руб. Остальные можно уже и не учитывать. С продажей в минус такая же история. За полчаса набросал алгоритм:

  • идём подряд по партиям (сделкам)
  • если это продажа, то:
    • вычитаем кол-во проданных акций из сделок покупок
    • отбрасываем сделки-покупки, в которых акций осталось 0
  • возвращаем подсписок, состоящий только из сделок-покупок, в которых кол-во акций > 0
def partial_accounting(deals: list[Transaction]) -> list[Transaction]:
    # не очень хорошая идея менять входящий массив
    # можно воспользоваться deepcopy
    start_item = 0

    try:
        for item in deals:
            if not item.reason:  # если продажа
                sell = item.count  # сколько осталось вычесть из партий
                while sell > 0:
                    left = deals[start_item].count  # акций осталось в партии-покупке
                    current = left - sell
                    if current > 0:
                        # в партии-покупке не всё исчерпали -> обновим количество и выходим
                        sell = 0
                        deals[start_item].count = current
                    else:
                        # партия-покупка распродана полностью
                        sell = sell - left  # обновляем сколько надо распродать
                        start_item += 1
                        while not deals[start_item].reason:
                            # ищем следующую партию-покупку
                            start_item += 1
        return [deal for deal in deals[start_item:] if deal.reason]
    except IndexError:
        # всё распродали
        return []

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

Простая визуализация

Из средств визуализации первое, что приходит на ум при работе с графиками - библиотека matplotlib. До текущего момента с ней не работал, так что за правильность использования API не отвечаю. Хочется на графике видеть покупки в виде кружочков, диаметр которых пропорционален количеству. Получилась такая функция:

def plot(key: str, deals: list[Transaction]):
    x_values = [x.timestamp.strftime('%d.%m.%Y') for x in deals]
    y_values = [float(x.price) for x in deals]
    sizes = [x.count for x in deals]

    fig, ax = pyplot.subplots()
    ax.scatter(x_values, y_values, s=sizes)  # непосредственно расставляем точки
    fig.autofmt_xdate()  # выравниваем даты для читаемости
    fig.set_size_inches(15, 10)  # увеличиваем масштаб
    pyplot.savefig(f'plots/{key}.png')
    pyplot.close(fig)  # не забываем закрыть внутренний буфер

И вот такой график:

По нему видно, что если я продам всю первую партию, то зафиксирую убыток в 350*k руб. С этого я сэкономлю 350*k*0.13 руб. налогов. Хоть что-то приятное. При этом ещё существенно скорректируется средняя, и психологически станет чуть легче.

Резюме

Тыжпрограммист снова нашёл себе применение. Вероятно, у вашего брокера уже есть подобный функционал, но не сложно его и самому допилить. Что вам делать с этой информацией я не знаю :) Моё дело алгоритмы для решения задач писать и подавать информацию в удобном виде. Всем поменьше рогатых на бирже.