...

суббота, 14 ноября 2020 г.

Как правильно и легко рассчитать прибыль на инвестиции или калькулятор ROI на Python

Суть проблемы

Пусть у вас есть вложения активов в некую стратегию (даже если buy and hold), и вы хотите рассчитать ROI(return on investment).

Если вы не производили никаких выводов или депозитов, тогда легко рассчитать прибыль
по формуле:

\\ROI = NAV / initial \: investments\\

где NAV- текущая стоимость наших активов, а initial \: investments - исходная стоимость активов.

Однако если в период инвестиций вы делали операции по счету, то их, конечно, нужно учитывать, и тогда простой формулы ROIздесь недостаточно. Одним из способов расчета доходности на ивестиции является расчет перефоманса цены акции "виртуального" паевого фонда (ПИФ). Думаю, что он многим знаком, а если нет, то покажется тоже интуитивным и простым после последующего описания и примеров (надеюсь).

Немного формул

При первом депозите надо создать "виртуальный" паевой фонд, начальное количество акций (паёв) в котором равно депонированным активам N(в акциях) с ценой за акцию P=1

Любой депозит или вывод средств в момент времени t эквивалентен покупке или продаже акций по цене P_t. Далее меняем состояние ПИФа при изменении счета по следующему алгоритму:

  1. ПустьXактивов было добавлено к фонду в момент времени T, где
    X > 0 при депозите и X < 0при выводе.

  2. В T_0 = T - \varepsilon ПИФ состоял из N акций с ценой P_{T_0} = {NAV}_{T_0} / N

  3. После выполнения транзакции, в момент времени T_1 = T + \varepsilonновое количество акций составит M = N + X / P_{T_0} а цена акции останется той же: P_{T_0} = P_{T_1} = NAV_{T_1} / M

Таким образом, для каждого момента времени tимеем:

  1. стоимость активов NAV_t

  2. количество виртуальных акций N_t

  3. цену одной акции P_t = NAV_t / N_t

В итоге, можно рассчитать доходность от начального момента времени
по формуле:

ROI = P_t / P_{t_0} - 1


Более того, также можно легко рассчитать ROI по этой формуле на любой период времени (t_0, t), t > t_0, в чем и заключается суть данного метода.

Пример

Допустим мы положили 100$ в стратегию. Сразу отметим, что в этот момент времени "покупаем" 100 акций за 1$. Далее стратегия за какое-то время заработала 20% и наш баланс теперь стал 120$, а следовательно изменилась и цена акции, она стала 120$ / 100 = 1.2$ (количество акций не изменилось, потому что никаких новых вложений или выводов не было).

Пусть в этот же момент времени мы решили положить ещё 210$, чтобы увеличить абсолютный доход. Депозит эквивалентен увелечению акций на 210$ / 1.2$ = 175. Таким образом, цена акции осталась (120 + 210)$ / (100 + 175) = 1.2$, а стоимость активов изменилась. Спустя время стратегия заработала ещё 10% от нового баланса, то есть стоимость активов стала равна 363$, следовательно стоимость акции стала равна 363$ / 275 = 1,32$.

Посчитаем доходность с начального момента до момента депозита: (1.2 / 1 - 1) * 100 = 20%
Посчитаем доходность от момента депозита: (1.32 / 1.2 - 1) * 100 = 10%
Посчитаем общую доходность на ивестиции: (1.32 / 1 - 1) * 100 = 32%

Наконец-то про код

Здесь мы будем манипулировать тремя простыми сущностями.

  1. транзакция (Transaction)

  2. ивестор (Investor)

  3. ПИФ (ROICalculator)

Транзакция является структурой с двумя полями, где funding - это вывод или депозит с соответсвующим знаком (X из формул)

class Transaction:
    '''
    Transaction model.

    timestamp: datetime.datetime - transaction timestamp
    funding: float - deposit or withdrawal
    {
        deposit: +X in asset [U]
        withdrawal: -X in asset [U]
    }
    '''
    def __init__(self, timestamp: datetime, funding: float):
        self.timestamp = timestamp
        self.funding = funding

Далее, модель инвестора - самая важная в рамках использования. Для расчетов нам важно иметь:

  1. начальный депозит

  2. дату первых инвестиций

  3. список транзакций

Cамое главное - переопределить метод доступа к балансу по временной метке. Best practice здесь запрос к БД или pandas.DataFrame

Transactions = List[Transaction]

class Investor(ABC):
    '''
    Investor model.

    1. Attributes
    investment_timestamp: datetime.datetime - investment timestamp (deposit timestamp)
    deposit: float - deposit amount in asset [U]
    transactions: Transactions - list of transactions with fundings and timestamp

    2. get_nav_by_timestamp - investor's net asset value

    '''

    def __init__(self, investment_timestamp: datetime, deposit: float, transactions: Transactions, *args, **kwargs):
        self.investment_timestamp = investment_timestamp
        self.deposit = deposit

        # sort transactions by timestamp
        # from first transaction to last
        #
        # EXCEPT DEPOSIT TRANSACTION
        #
        self.transactions = sorted(
            transactions, key=lambda x: x.timestamp, reverse=False)

    @abstractmethod
    def get_nav_by_timestamp(self, timestamp: datetime) -> float:
        '''returns NAV'''
        raise NotImplementedError

И последнее - сам ROICalculator. В целом, он полностью повторяет алгоритм, описанный выше, сохраняя состояние ПИФа в атрибуты объекта, что позволяет достаточно быстро рассчитывать share price на любой момент времени tдаже на больших данных с большим количеством движений по счету (проверял на боевых данных).

class ROICalculator:
    '''
    ROICalculator.

    1. Create virtual pif __init_pif 
    {
        init shares = deposit quantity of asset[U]
        share price = 1
    }

    2. System go through 3 conditions while getting funding
    {
        Let funding X[U] was added to virtual pif at T;

        T - transaction timestamp,
        T0 = T - eps - timestamp before transaction
        T1 = T + eps - timestamp after transaction

        pif consisted of N SHARES with share price P_0[U] = NAV_T0[U] / N.

        Add X[U] to virtual pif: M = N + X[U] / P_0[U],
        where M - new shares amount

        Update share price P[U] = NAV_T1[U] / M

    }
    '''

    def __init__(self, investor: Investor, eps_hours=1):
        # eps is used while getting nav_before
        # and nav_after transaction
        self.investor = investor
        self.eps_hours = eps_hours
        self.__init_pif()

    def __init_pif(self):
        self.shares = self.investor.deposit
        self.share_price = 1

    def __calculate_shares(self, funding: float):
        self.shares += funding / self.share_price

    def __calculate_share_price(self, nav: float):
        self.share_price = nav / self.shares

    def __calculate_shares_by_timestamp(self, timestamp: datetime):

        # create virtual pif each time calculating shares
        self.__init_pif()

        for transaction in self.investor.transactions:
            if transaction.timestamp > timestamp:
                break

            # 1 condition: before transaction
            # T0
            timestamp_before_transtaction = transaction.timestamp - \
                timedelta(hours=self.eps_hours)

            if timestamp_before_transtaction < self.investor.investment_timestamp:
                nav_before = self.investor.deposit

            # NAV_T0
            try:
                nav_before = self.investor.get_nav_by_timestamp(
                    timestamp_before_transtaction)
            except Exception as e:
                print(e)

            # P0 = NAV_T0 / N
            self.__calculate_share_price(nav_before)

            # 2 condition: add funding to virtual pif
            # shares = M
            self.__calculate_shares(transaction.funding)

            # T1
            timestamp_after_transtaction = transaction.timestamp + \
                timedelta(hours=self.eps_hours)

            # NAV_T
            try:
                nav_after = self.investor.get_nav_by_timestamp(
                    timestamp_after_transtaction)
            except Exception as e:
                print(e)

            # update share price
            # P[U] = NAV_T1[U] / M
            self.__calculate_share_price(nav_after)

    def __calculate_share_price_by_timestamp(self, timestamp: datetime):
        # update shares N in self.shares
        self.__calculate_shares_by_timestamp(timestamp)

        # get NAV from data
        nav = self.investor.get_nav_by_timestamp(timestamp)

        # update share_price in self.share_price
        self.__calculate_share_price(nav)

    def get_share_price_perfomance(self, t0: datetime, t: datetime) -> float:
        '''
        t  - end_timestamp
        t0 - start_timestamp, t > t0

        t = datetime.utcnow(), t0 = investment_timestamp to get ROI
        '''
        self.__calculate_share_price_by_timestamp(t)
        # fix share_price at t
        k = self.share_price

        self.__calculate_share_price_by_timestamp(t0)
        # fix share_price at t0
        k0 = self.share_price

        return k / k0 - 1

Как можно использовать

Допустим, вы положили средства в лендинговую стратегию с доходом около 0.05% в день на инвестированные средства. Это означает, что наш P&L на стоимость активов будет рассчитываться как:

periods_i - days \: between \: transaction_i \: and\: transaction_{i-1}, \\ investments_i - total \: investments \: in \: the \: period_iPnL_t = 0.005 * \sum_{i=1}^n investments_i * period_i, \: n - transactions \: number \: before \: t

Это нужно для правильного определения доступа к балансам по временной метке.

Пусть 2020/1/1 было депонировано 100$, а 2020/4/1, было депонировано ещё 200$, тогда, с учетом описанной выше формулы получаем такую модель инвестора:

class ExampleInvestor(Investor):
    '''

    Simple lending (static) strategy with 0.05% profit daily
    on investments without reinvestment

    '''

    def __init__(self, investment_timestamp, deposit, transactions):
        super().__init__(investment_timestamp, deposit, transactions)

    def lending_assets(self, timestamp):
        # before transaction
        if timestamp <= datetime(2020, 4, 1):
            return 100
        # after transaction
        else:
            return 300

    def get_nav_by_timestamp(self, timestamp):
        '''

        NAV = investments + PnL
        daily PnL = 0.0005 * investments =>
        total PnL = 0.0005 * sum(invesmetns_i * period_i)

        '''
        if timestamp < datetime(2020, 4, 1):
            pnl = 0.0005 * \
                self.lending_assets(timestamp) * \
                (timestamp - self.investment_timestamp).days
            return self.lending_assets(timestamp) + pnl

        elif timestamp > datetime(2020, 4, 1):
            # redefine investments_i and daily PnL
            transaction_timestamp = datetime(2020, 4, 1)
            acc_pnl_before_transaction = 0.0005 * self.lending_assets(
                transaction_timestamp) * (transaction_timestamp - self.investment_timestamp).days
            pnl =  0.0005 * self.lending_assets(timestamp) * (timestamp - transaction_timestamp).days +\
                acc_pnl_before_transaction

            return self.lending_assets(timestamp) + pnl

Определим модель инвестора:

transaction = Transaction(datetime(2020, 4, 1), funding=200)
investor = ExampleInvestor(investment_timestamp=datetime(2020, 1, 1),
                           deposit=100, transactions=[transaction])

Создадим модель ПИФа:

pif = ROICalculator(investor)

И теперь при помощи метода get_share_price_perfomance можем получить ROI на любой период времени. В качестве примера посчитаем 1D%, MTD% и YTD% до и после депозита и получим:

1D return on 2020-03-31 = 0.05 %
MTD return on 2020-03-31 = 1.51 %
YTD return on 2020-03-31 = 4.50 %

1D return on 2020-04-30 = 0.05 %
MTD return on 2020-04-30 = 1.44 %
YTD return on 2020-04-30 = 6.01 %

Делюсь кодом в надежде на то, что это кому-нибудь ещё пригодится и пару часов моих выходных не прошли впустую. Лично у меня получилось очень удачно совместить эту небольшую модель с API бирж, а также используя известную питоновскую ORM - sqlalchemy для доступа к балансам.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий