Суть проблемы
Пусть у вас есть вложения активов в некую стратегию (даже если buy and hold), и вы хотите рассчитать (return on investment).
Если вы не производили никаких выводов или депозитов, тогда
легко рассчитать прибыль
по формуле:
где - текущая стоимость наших активов, а - исходная стоимость активов.
Однако если в период инвестиций вы делали операции по счету, то их, конечно, нужно учитывать, и тогда простой формулы здесь недостаточно. Одним из способов расчета доходности на ивестиции является расчет перефоманса цены акции "виртуального" паевого фонда (ПИФ). Думаю, что он многим знаком, а если нет, то покажется тоже интуитивным и простым после последующего описания и примеров (надеюсь).
Немного формул
При первом депозите надо создать "виртуальный" паевой фонд, начальное количество акций (паёв) в котором равно депонированным активам (в акциях) с ценой за акцию
Любой депозит или вывод средств в момент времени
эквивалентен покупке или продаже акций по цене
. Далее меняем состояние ПИФа при изменении счета по следующему
алгоритму:
-
Пустьактивов было добавлено к фонду в момент времени , где
при депозите и при выводе. -
В ПИФ состоял из акций с ценой
-
После выполнения транзакции, в момент времени новое количество акций составит а цена акции останется той же:
Таким образом, для каждого момента времени имеем:
-
стоимость активов
-
количество виртуальных акций
-
цену одной акции
В итоге, можно рассчитать доходность от начального момента
времени
по формуле:
Более того, также можно легко рассчитать
по этой формуле на любой период времени
, в чем и заключается суть данного метода.
Пример
Допустим мы положили 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%
Наконец-то про код
Здесь мы будем манипулировать тремя простыми сущностями.
-
транзакция (
Transaction
) -
ивестор (
Investor
) -
ПИФ (
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
Далее, модель инвестора - самая важная в рамках использования. Для расчетов нам важно иметь:
-
начальный депозит
-
дату первых инвестиций
-
список транзакций
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 на любой момент времени
даже на больших данных с большим количеством движений по счету
(проверял на боевых данных).
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 на стоимость активов будет рассчитываться как:
Это нужно для правильного определения доступа к балансам по временной метке.
Пусть 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 для доступа к балансам.