Суть проблемы
Пусть у вас есть вложения активов в некую стратегию (даже если 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 для доступа к балансам.
nckma
Мне кажется, что очень многое не учтено.
Например, дивиденды по акциям. Казалось бы можно их считать просто депозитом, но это не верно ведь?
Депозит, как внесение новых средств, это то, что вы отрезаете от своей зарплаты с целью инвестирования. А дивиденды это другое. Это прямо результат инвестирования. Получение дивиденда увеличивает РОИ, а внесение депозита уменьшает, ведь внесенные средства когда еще отобьются.
Второе. А как насчет учета НДФЛ? Какой бы ни был инвестор бай&холд, он все же иногда продает покупает акции, например, делает ребалансировку портфеля, продает сильно выросшие и покупает подешевевшие акции, или, к примеру, продает акции покинувшие индекс и покупает акции компаний попавших в индекс.
Правильный учет налога НДФЛ важен. По российскому законодательству акции учитываются в режиме ФИФО. Первую купил, она же первая и будет продана. Отсюда четко понятно получена ли прибыль от продажи или нет. Из суммы прибылей/убытков потом в конце года считается размер налога к уплате. И этот налог так же влияет на РОИ.
marvincfs
Дивиденд должен увеличить цену пая, не меняя их количество.
А вот с НДФЛ еще и льготы можно учитывать, чтоб совсем не скучать.