21 сен 2018 Александр Мыльцев Все авторы
Большинство из финансистов, особенно из числа «старой школы», привыкло делать расчеты в EXCEL. Кто-то стал экспертом в этой области и автоматизировал всё или почти всё с помощью макросов и VBA. Но все-таки, каким бы ни был прекрасным старый добрый EXCEL, его возможности не безграничны, как в области работы с переменными и типами данными, так и по части быстродействия. Много и других проблем. Например, в плане приспособления к табличному «интерфейсу» ввода/вывода. Насколько хорош EXCEL для решения простейших математических задачек, настолько он и плох для более серьезных вычислений, где требуется повторяемость и автоматизация.
Поэтому мы решили поделиться своим опытом в области решения финансовых задач, особенно связанных с Современной теорией портфеля (СТП) и распределением активов, с помощью одного из наиболее популярных и простых в освоении языков современности – Python.
Этой статьей мы начинаем серию публикаций, где расскажем о подходах, которые можно использовать для вычисления основных параметров инвестиционных портфелей. Материалы рассчитаны на читателя, уже знакомого с основами Python и математики портфеля.
Портфельному инвестору необходимо знать довольно много расчетных показателей. В перовой статье мы расскажем про базовые показатели единственного актива: определения, математические формулы и программный код, который вычисляет их значения. Для Python существует множество готовых библиотек, которые помогают решать задачи быстро и элегантно, без необходимости генерировать страницы лишнего кода.
Для вычислений в СТП мы используем:
numpy
- для векторных вычисленийpandas
- для работы с табличными даннымиcvxopt
- для решения оптимизационных задачflask
иgraphene
- для предоставления GraphQL API (это важно, хотя и не относится напрямую к вычислениям)
Расчёт всех основных показателей портфеля чаще всего производится на основе месячных данных закрытия торгов. Можно использовать и дневные данные, но это приводит к некоторым сложностям, связанными с экстраполяцией результатов от масштабов дня к масштабу года (а именно в годовом измерении все привыкли видеть итоговые результаты). Годовые данные тоже можно использовать, но лишь в тех случаях, когда присутствует достаточный объем статистических данных. В реальности же мы имеем ситуацию, когда в большинстве случаев глубина данных для биржевых активов ограничена 10-20 годами в лучшем случае. Особенно такие ограничения характерны для российских активов.
Обозначим данные закрытия как \(Close(t_k), \ k=0,\ldots,n\) - от начального месяца с индексом 0 до месяца с индексом n. Например, для индекса S&P 500 за 13 месяцев с января 2016 года такими значениями будут:
> import numpy as np
> close = np.array([184.46203524, 184.30967886, 196.70350767, 197.47875798,
200.83817857, 201.54059247, 208.89103144, 209.14118692,
209.15747242, 205.5313027 , 213.10274701, 217.42514811,
221.31590369])
13 значений данных закрытия в этом примере нужны для того, чтобы получить из них 12 значений доходности.
Доходность
Доходность (Rate of Return) – это относительный прирост стоимости актива в каждом из периодов по отношению к предыдущему значению:
$$ROR(t_i) = \frac{ Close(t_i)}{ Close(t_{i-1})} - 1 , \ i = 1,\ldots,n$$
Для внутреннего представления временных рядов мы используем np.array
, в котором нет встроенной операции, которая бы вычисляла относительный прирост, как например pct_change()
в Pandas
. Поэтому мы вычисляем так:
> ror = np.diff(close) / close[:-1]
> ror
array([-8.25949820e-04, 6.72445901e-02, 3.94121246e-03, 1.70115541e-02,
3.49741224e-03, 3.64712581e-02, 1.19754056e-03, 7.78684414e-05,
-1.73370317e-02, 3.68383999e-02, 2.02831787e-02, 1.78946898e-02])
Обратим внимание на два важных момента:
- Деление применяется поэлементно для векторов
- В результате вычислений получилось 12 значений, а не 13. Выдумывать несуществующие значения, чтобы длина вектора
close
была равна длине вектораror
– чревато ошибками
Накопленная доходность
Накопленная доходность (Accumulated Rate of Return) – это итоговая доходность актива за определенный промежуток времени. С точки зрения математики накопленная доходность - кумулятивное произведение доходностей от начальной до текущей за каждый из периодов:
$$AROR(t_i) = \left( \prod_{j=1}^i \left( ROR(t_j) + 1 \right) \right) - 1, \ i = 1,\ldots,n$$
Вектор aror
вычисляется в Python так:
> aror = (ror + 1.).cumprod() - 1.
> aror
array([-0.00082595, 0.0663631 , 0.07056586, 0.08877785, 0.09258576,
0.13243373, 0.13378987, 0.13387816, 0.11422007, 0.15526616,
0.17869863, 0.19979108])
Среднегодовая доходность
Среднегодовая доходность (Compound Annual Growth Rate) – это усреднённая доходность (среднее геометрическое) на выбранном периоде:
$$CAGR(t_1, t_i) = \left( \frac{ AROR(t_i) + 1}{ AROR(t_1) + 1} \right)^{\frac1{ t_i - t_1}} - 1, \ i = 1,\ldots,n$$
Обратим внимание на то, что \(t_i - t_1\) не обязательно целое число. Термин CAGR удобно использовать, так как он довольно часто используется в мире финансов - Compound annual growth rate и позволяет отличить среднегодовую доходность от, например, матожидания доходности.
Вычислим в Python значение CAGR для всего периода:
> years_total = aror.size / 12
> cagr = (aror[-1] + 1.) ** (1 / years_total) - 1.
> cagr
0.19979107573297994
Риск
Мы различаем два типа риска: месячный и приведённый к году. Приведенное к году значение наиболее важно, так как зачастую является главным итогом вычислений и важным параметром для конкретного актива. Приведенным этот параметр называется потому, что является расчётным. Его значение вычисляется и приводится к году, исходя из месячных показателей портфеля (риск и доходность).
Месячный риск равен стандартному отклонению месячных значений доходности:
$$Risk_{monthly} = \sqrt{\frac{ \sum_{i=1}^{n}\left(ROR(t_i)-{\overline{ROR}}\right)^{2}}{n-1}}, \ i = 1,\ldots,n$$
где
$$\overline{ROR} = \frac{ \sum_{i=1}^n ROR(t_i) }{n}$$
Риск, приведённый к году вычисляется по формуле:
$$Risk_{yearly} = \sqrt{\left(Risk_{monthly}^2 + \overline{ROR}^2\right)^{12} - \overline{ROR}^{24}}$$
С выводом этой формулы можно ознакомиться в нашей статье Приведение месячных данных к годовым. Как избежать ошибки.
Важно отметить, что формула не ограничивает длину вектора $$ROR. $$Но на практике Global Investment Performance Standards (GIPS) не рекомендует приводить к году данные для периодов меньше года. Именно поэтому мы в начале статьи указали на необходимость загрузки минимально 13 значений данных закрытия. Это дает возможность получить приведенные к году значения риска и доходности (CAGR).
Соответствующие значения в Python вычисляются так:
> risk_monthly = ror.std()
> risk_monthly
0.021733982481594843
> ror_mean = (1. + ror).mean()
> risk_yearly = np.sqrt((risk_monthly**2 + ror_mean**2)**12 - ror_mean**24)
> risk_yearly
0.08930419487025891
Инфляция
Инфляция - это изменение показателя индекса потребительских цен (обозначаем как CPI_Rate) за промежуток времени в процентах.
Общепринятым подходом для вычисления средней инфляции за промежуток времени является среднее геометрическое (так же как и для доходности). Но математический смысл имеет и среднее арифметическое - математическое ожидание инфляции. Мы вычисляем три значения на основе данных индекса потребительских цен: математическое ожидание инфляции (среднее арифметическое), средняя инфляция (среднее геометрическое), накопленная инфляция. Накопленная инфляция, подобно доходности – кумулятивное произведение значений.
Инфляция через индекс потребительских цен:
$$I(t_i) =CPI_{Rate} (t_i) - CPI_{Rate} (t_{i-1})$$
Формула верна для российской инфляции, так как в России индекс потребительских цен считается в процентах. В США и ЕС индекс считается в пунктах, а инфляция вычисляется через относительный прирост индекса.
Математическое ожидание инфляции:
$$I_{arithmetic\ mean} = \overline{I} = \frac{ \sum_{i=1}^n I(t_i) }{n}$$
Накопленная инфляция:
$$I_{accumulated} = \prod_{j=1}^{i} (I_j + 1) - 1$$
Средняя инфляция:
$$I_{geometric\ mean} = G(I) = \left( \prod_{i=1}^{n} (I(t_i) + 1) \right)^{1/(n/12)} - 1 $$
$$= \left( I_{accumulated}(t_n) + 1 \right)^{1/(n/12)} - 1$$
Вычисление:
# US inflation for period from 2016-02 to 2017-1
> inflation = np.array([0.000823, 0.004306, 0.004741, 0.004046, 0.003284, -0.001618,
0.000918, 0.002404, 0.001247, -0.001555, 0.000327, 0.005828])
> inflation_arithmetic_mean = inflation.mean()
> inflation_arithmetic_mean
0.0020625833333333334
> inflation_accumulated = (inflation + 1.).cumprod() - 1.
> inflation_accumulated
array([0.000823 , 0.00513254, 0.00989788, 0.01398392, 0.01731385,
0.01566783, 0.01660022, 0.01904412, 0.02031487, 0.01872828,
0.01906141, 0.0250005 ])
> years_total = inflation.size / 12
> inflation_geometric_mean = (inflation_accumulated + 1.)**(1 / years_total) - 1.
> inflation_geometric_mean
0.025000495851904336
Реальные значения
Реальные значения – значения, скорректированные на значение инфляции. Реальные значения обычно вычисляются для доходности, накопленной доходности, средних доходностей.
Реальная доходность (один месяц):
$$ROR_{real}(t_i) = \frac{ ROR(t_i) + 1}{ I(t_i) + 1} - 1, \ i = 1,\ldots,n$$
Реальная накопленная доходность:
$$AROR_{real}(t_i) = \frac{ AROR(t_i) + 1}{ I_{accumulated}(t_i) + 1} - 1, \ i = 1,\ldots,n$$
Среднегодовая реальная доходность:
$$CAGR_{real}(t_i) = \frac{ CAGR(t_i) + 1}{ \left(I_{accumulated}(t_i) + 1\right)^{1/(i/12)}} , \ i = 1,\ldots,n$$
Вычисляются значения в Python так:
> ror_real = (ror + 1.) / (inflation + 1.) - 1.
> ror_real
array([-0.00164759, 0.06179488, -0.00589828, 0.00298588, -0.01358129,
0.02048251, -0.01515116, -0.01861181, -0.03690224, 0.01777718,
0.00119892, -0.00693249])
> aror_real = (aror + 1.) / (inflation_accumulated + 1.) - 1.
array([-0.00164759, 0.06091789, 0.06007339, 0.07376244, 0.07399084,
0.11496465, 0.11527605, 0.11268799, 0.09203551, 0.13402777,
0.15665123, 0.17052731])
> cagr_real = (cagr + 1.) / (inflation_accumulated[-1] + 1.)**(1/years_total) - 1.
> cagr_real
0.17052731251198328
Следующая статья цикла: Python: Базовые показатели портфеля

Похожие материалы:
— Финансовая библиотека с открытым кодом на Python - yapo
— Python: Базовые показатели портфеля
— Бонус за ребалансировку портфеля: теория и практика
— Приведение месячных данных портфеля к годовым. Как избежать ошибки
— Геометрическая разница, считаем реальную доходность
— Где интуиция не срабатывает: считаем доходность
— Формулы древних: доходность инвестиций
— Python: Тип данных для финансовых вычислений или как избежать ошибок
Warning: "continue" targeting switch is equivalent to "break". Did you mean to use "continue 2"? in /var/www/rostsber/data/www/rostsber.ru/core/components/jevix/model/jevix/jevix.class.php on line 121
Добрый день! Я новичок в этом деле. Подскажите, пожалуйста, могу ли я из цен закрытия на дату получить месячные значения или каким-то образом рассчитать годовую доходность сразу на этом массиве? Если, да, то как? Заранее спасибо.
Тогда один из популярных вариантов такой:
1. Создаете DataFrame с ежедневными ценами закрытия — df
2. Считаете дневные доходности: df.pct_change()
3. Удаляем первое значение (т.к. оно будет равно NaN): df = df.iloc[1:]
4. Через ресэмплинг получаете месячные данные по доходности: df.resample('M').apply(lambda x: (np.prod(1 + x) — 1))
Если нужны годовые данные доходности, то меняем период ресемплинга:
df.resample('A').apply(lambda x: (np.prod(1 + x) — 1))
Скажите, пожалуйста, могу ли я использовать groupper вместо resample?
df.groupby(pd.Grouper(freq='M', level=0)).apply(lambda x: (np.prod(1 + x)-1))
Правильно ли понимаю, что если я буду использовать группировку по году, это и будут мои финальные значения доходности?
df.groupby(pd.Grouper(freq='Y', level=0)).apply(lambda x: (np.prod(1 + x)-1))
Как если бы у меня были месячные данные, а я бы проделывала те же манипуляции, что и в статье?
2) Получите те же данные. В любом случае мы перемножаем ежедневные доходности (r+1) между собой. Но делаем это разными способами. Есть и другой способ. Сгруппировать данные закрытия по году, взять последние даты закрытия в каждой группе при помощи .last(), потом вычислить доходность при помощи соотношения между данными закрытия по году через .pct_change()