Суть проблемы
Пусть у вас есть вложения активов в некую стратегию (даже если 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 для доступа к балансам.