Как правильно и легко рассчитать прибыль на инвестиции или калькулятор 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 для доступа к балансам.

ссылка на оригинал статьи https://habr.com/ru/post/527722/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *