Новые релизы okama

Версия 1.1.1
Ноябрьский релиз с небольшими дополнениями и исправлениями доступен через pip.

Ежегодное отклонение от индекса

Новый метод .tracking_difference_annual для AssetList позволяет рассчитать историю отклонения от бенчмарка по календарным годам. Метод может быть полезен для проверки стабильности качества управления фондом. Например, это график ежегодных отклонения двух популярных фондов на S&P500: VOO (Vanguard, NYSE) и SPXS (Invesco UCITS, LSE).
okama-AssetList-tracking_difference_annual-1

Подробное описание нового метода и примеры использования доступны в Документации.

Исправление ошибок

Перед инициализацией классы AssetList, Portfolio, EfficientFrontier проверяют активы на достаточность исторического периода данных. Теперь в список активов можно включить только тикеры, имеющие как минимум 3 месяца истории. В противном случае - ValueError.

Версия 1.1.2
Релиз посвящен работе с популярными финансовыми коэффициентами. Обновление доступно через pip.

Коэффициент Сортино

  • метод get_sortiono_ratio() доступен в классах AssetList и Portfolio

Коэффициент Сортино является аналогом Коэффициента Шарпа. Разница в том, что в знаменателе учитывается не волатильность а полуотклонение (среднеквадратичное отклонение ниже целевой доходности, downside deviation):

S = \frac{R_p-R_t}{DR}

R_p - матожидание доходности портфеля (среднее арифметическое доходности)
R_t - целевая доходность портфеля
DR - полутоклонение доходности

Пример сравнения Коэффициента Сортино для различных активов:

al = ok.AssetList(['VOO.US', 'BND.US'], last_date='2021-12')
al.get_sortino_ratio(t_return=0.03)  # целевая доходность равна 3%
VOO.US    1.321951
BND.US    0.028969

Коэффициент Сортино для инвестиционного портфеля:

pf = ok.Portfolio(['VOO.US', 'BND.US'], last_date='2021-12')
pf.get_sortino_ratio(t_return=0.02)
1.4377728903230174

Коэффициент диверсификации

Коэффициент диверсификации показывает на сколько эффективно применяется корреляция для снижения риска в портфеле. В отличие от коэффициентов Шарпа и Сортино в формуле не используется доходность. Речь идет только о снижении риска:

DR(w) = \frac{\sum_{i=1}^{N} w_i \times \sigma_i}{\sigma}

w_i - вес ценной бумаги в портфеле
\sigma_i - риск (среднеквадратическое отклонение) доходности ценной бумаги
\sigma - риск инвестиционного портфеля

Из формулы хорошо видно, что коэффициент диверсификации (КД) всегда больше единицы. Взвешенная сумма рисков активов всегда больше или равна риску портфеля.
Чем больше КД, тем выше снижен риск за счет низкой корреляции.

  • атрибут diversification_ratio доступен для Portfolio
  • оптимизации по КД возможна в классе EfficientFrontier через метод get_most_diversified_portfolio(). Метод позволяет получить как глобальный оптимизированный портфель, где КД достигает максимальной величины, так и локально оптимизированный портфель для заданной доходности портфеля.
  • множество оптимизированных по КД портфелей доступно через атрибут mdp_points в EfficientFrontier. Оптимизация происходит последовательно для промеж

Преимущество оптимизированных по КД портфелей заключается в том, что в отличие от Границы эффективности в них нет “вырожденных” весов. Поэтому для конструирования реальных инвестиционных портфелей распределение по КД - это хорошая подсказка.

ВНИМАНИЕ: использовать в явном виде эти веса нельзя, так же как и веса портфелей на Границе эффективности.

Пример построения кривой максимально диверсифицированных портфелей и Границы эффективности. В примере использованы портфели, составленные из:

  • Индекс S&P500 полной доходности (SP500TR.INDEX)
  • Индекс Мосбиржи полной доходности (MCFTR.INDEX)
  • Индекс облигаций федерального займа Мосбиржи (RGBITR.INDX)
  • Спотовые цены на золото (GC.COMM)
ls4 = ['SP500TR.INDX', 'MCFTR.INDX', 'RGBITR.INDX', 'GC.COMM']
y = ok.EfficientFrontier(assets=ls4, ccy='RUB', n_points=100)  
# n_points=100 задает количество точек на Границе эффективности и на кривой 
# максимально диверсифицированных портфелей

mdp = y.get_most_diversified_portfolio()   # Глобальный максимально диверсифицированный портфель
mdp
{'SP500TR.INDX': 0.18734372897034768,
 'MCFTR.INDX': 0.023902608406034215,
 'RGBITR.INDX': 0.6595979466663215,
 'GC.COMM': 0.12915571595729658,
 'Mean return': 0.12279521203933563,
 'CAGR': 0.12124953230039481,
 'Risk': 0.059120398386235534,
 'Diversification ratio': 1.9671253973462728}

df = y.mdp_points  # множество локально оптимизированных по КД портфелей (100 шт)
ef = y.ef_points  # портфели на Границе эффективности (100 шт)

Строим графики Границы эффективности, Кривой максимально диверсифицированных портфелей, отображаем точку, где КД максимален:

fig = plt.figure()
# Отображение точек риск-доходность для каждой ценной бумаги
y.plot_assets(kind='cagr')  # kind should be set to "cagr" as we take "CAGR" column from the ef_points.
ax = plt.gca()
# Отображение Границы эффективности
ax.plot(ef['Risk'], ef['CAGR'], label='Efficient Frontier')  # на оси Y мы отображаем среднегодовую доходность (CAGR)
# Отображение кривой с максимально диверсифицированными портфелями
ax.plot(df['Risk'], df['CAGR'], linestyle='dashed', label='Most diversified portfolios')
# Отображение точки с максимальным КД (глобальный максимум). Обозначен как MDP
ax.scatter(mdp['Risk'], mdp['CAGR'], s=30, marker='o', label='MDP')
# Название графика и надписи осей
ax.set_title('Efficient Frontier vs Most diversified portfolios frontier')
ax.set_xlabel('Risk (Standard Deviation)')
ax.set_ylabel('Return (CAGR)')
ax.legend();

Версия 1.1.3
Небольшой релиз, связанный с переходом на poetry для управления зависимостями библиотеки (до этого зависимости управлялись с помощью requirements.txt).

Кроме того, исправлен неприятный баг, приводивший к некорректному определению границ доступного исторического периода для Portfolio, AssetList и зависимых классов.

Обновление доступно через pip.

Версия 1.1.5
Релиз связан с переходом на новый API финансовой базы данных.
Новый сервер расположен по адресу api.okama.io:5000.

Рекомендую обновиться до новой версии. API по старому адресу будет поддерживаться еще примерно месяц. Уже сейчас по старому адресу не доступны новые типы данных (например расширенная база курсов валют ЦБ .CBR)

Пример запроса на новом API:
http://api.okama.io:5000/api/namespaces/

Версия 1.1.6
Параллельные вычисления теперь используются в “тяжелых” расчетах, связанных с оптимизацией портфеля с произвольной ребалансировкой портфеля (расчет Границы эффективности). Спасибо за помощь @alex

Для параллельных вычислений используется библиотека Joblib. Прирост скорости составил примерно 20%, но будет гораздо выше, когда сможем перейти на Pandas версии 1.4 и выше. Пока для совместимости с Python 3.7 используется Pandas версии 1.1.5. Версия Python 3.7 будет поддерживаться еще какое-то время до тех пор пока Google Colab не перейдет но более новую версию.

Другие изменения:

  • библиотека black используется для автоформатирования стиля кода
  • в зависимостях в версии для разработчика по умолчанию используется Python 3.7. При текущих зависимостях okama работает с версиями 3.7, 3.8 и 3.9

Версия 1.2.0

Новый класс для макроэкономических параметров Indicator

Теперь 3 макро-класса доступны (и отображены в Документации):

  • Indicator : Макроэкономические индикаторы. (.RATIO новый namespace)
  • Inflation : Инфляция и свойственные для нее методы (.INFL namespace)
  • Rates : Ставки банков и Центробанков (.RATE namesapce)

Циклический P/E Шиллера (CAPE10) для 20+ стран добавлен в базу данных. Новые тикеры: USA_CAPE10.RATIO, CHN_CAPE10.RATIO, CHN_CAPE10.RATIO и т.д.

Дневные данные в Макроэкономических классах

Rate класс теперь имеет .values_daily атрибут (по аналоги с Asset().daily_close):
ok.Rate("RUONIA.RATE").values_daily

Все макроэкономические классы имеют .values_monthly атрибут.

.describe() метод для все макро-классов

.describe() показывает статистику с начала года и для выбранного списка временных промежутков:

  • среднее (арфметическое)
  • медиана
  • макс. и мин. значения с датами

.describe() в классе Inflation отличается. Он показывает характерные для инфляции статистические величины:

  • Инфляция с начала года (YTD)
  • Среднегодовая инфляция (среднее геометрическое) для выбранных периодов и за весь срок
  • Максимальная 12-месячная инфляция для выбранных периодов и за весь срок
  • Покупательная способность для 1000 денежных единиц (для выбранных периодов и за весь срок)

Скользящие отклонения от индекса для ETF

.tracking_difference() и tracking_difference_annualized() теперь являются методами (были атрибутами). У них появился необязательный параметр rolling_window который задает размер окна для скользящего отклонения в месяцах.
Например, для расчета скользящего 24-месячного отклонения:
x.tracking_difference(rolling_window=24)

Небольшая иллюстрация, для чего могут быль полезны скользящие отклонения в сравнении индексных фондов. Они могут быть полезны, когда накоплена достаточно глубокая статистики наблюдений и требуется посмотреть наиболее актуальные данные (а не в глубине истории).

Сравнение точности следования SPY и VFINX

Сравним отклонение самого старого ETF на S&P500 SPDR c тикером SPY и первого индекса взаимного фонда от Vanguard (тикер VFINX).

x = ok.AssetList(['SP500TR.INDX', 'SPY.US', 'VFINX.US'])
x

image
Общая история у этих фонов доступна за последние 29 лет.
Можно посмотреть, кто из них отклонился больше за весь промежуток:

x.tracking_difference().plot()


Очевидное преимущество у взаимного фонда Vanguard.
Посмотрим тот же результат, но приведенный к готовым значениям:

x.tracking_difference_annualized().plot()


Видно, что в начале периода наблюдения фонд Vanguard несколько опережал индекс (как ему это удавалось - это отдельная история). SPY в среднем отставал от индекса на 1,5% за последние 29 лет.

При выборе между двумя этими фондами информации явно недостаточно. Инвестору важно знать, как фонд управлялся в последние 5-10 лет а не в начале 90х…
Для этого посмотрим скользящие отклонения, например, с окном в 5 лет:

x.tracking_difference_annualized(rolling_window=12*5).plot()


Мы видим, что в последние годы отклонения у индексных фондов не так сильно отличаются. Оба фонда отстают от SP500 TR менее чем на 1% ежегодно. У VFINX показатели более ровные и, начиная с 2020 года, несколько лучше чем у SPY. Секрет, как обычно, может быть в TER, но не только …

Версия 1.2.1
Для функции get_tangency_portfolio() в классе EfficentFrontier добавлен новый параметр cagr. При cagr=True функция рассчитывает точку с максимальный Коэффициентом Шарпа (MSR) для среднегодовой доходности, рассчитанной с помощью среднего геометрического (CAGR). По умолчанию, как и раньше, параметры MSR считаются для среднего арифметического доходности (для матожидания).

Исправления:

  • в классах AssetList и Portfolio теперь правильно считаются first_date и last_date для базовой валюты.

Версия 1.2.2 - последний релиз, поддерживающий Python 3.7

Исправления:

  • баг с совместимостью старой библиотеки importlib-metadata

Обновления:

  • новый формат данных FOREX потребовал обновления классов-наследников ListMaker (Portfolio, AssetList, EfficentFrontier и т.п.)

ВНИМАНИЕ: С новым форматом данных FOREX (он уже реализован в API) старые релизы библиотеки работать не будут. Требуется обновиться до 1.2.2

Разработка следующих версий библиотеки будет вестись с помощью Python 3.8

Версия 1.2.3 - релиз с минимальной версией Python 3.8

Новые версии okama больше не будут поддерживать легаси Python 3.7. Исключение старых версий Python было необходимо для корректной работы с новыми возможностями в Pandas, SciPy и Numpy. Старые версии питона постепенно перестают поддерживаться этими библиотеками.

Обновления:

  • метод EfficentFrontier().get_monte_carlo() теперь кроме риска и доходности возвращает веса сгенерированных портфелей

Исправления:

  • Portfolio().weights_ts возвращает правильный порядок активов, входящих в портфель

ВНИМАНИЕ: Google Colab все еще работает на Python 3.7. Пока они не перейдут на 3.8+ !pip install okama будет устанавливать версию 1.2.2 okama.

Версия 1.3.0 - Новые возможность для скользящих. Sequencies

Новые возможности библиотеки

  • параметр rolling_window доступен в функциях AssetList: index_corr(), index_beta(), tracking_error(). Теперь можно строить скользящие для Ошибки следования и для беты.
  • Две функции index_corr() и index_rolling_corr() слиты в одну в AssetList
  • AssetList, Prtfolio, EfficentFrontier и EfficentFrontierReb теперь являются sequences (последовательностями) и обладают методами __getitem__, __iter__.

Исправления ошибок

  • get_namspaces() и другие алиасы из init.py больше не совершают удаленных запросов по API при импорте (спасибо за помощь @alex)
  • EfficientFrontier.plot_pair_ef() падала при inflation=False
  • Можно использовать тикеры с точкой. Такие как BRK.B

Скользящая ошибка следования американских и европейских (UCITS) индексных ETF на S&P 500

Примеры использования новых возможностей версии 1.3.0.

Выбираем самые популярные индексные ETF на NYSE и LSE:
image

Строим 2-х летнюю скользящую для Ошибки следования (Tracking Error):

Разница впечатляет … не так ли?
0,2% ошибка следования американских ETF и около 5% для европейского фонда CSPX на Лондонской бирже.

Недавно опубликовал статью Методика сравнения индексных фондов. Tracking Difference и Tracking Error. Там подробно объясняются, откуда берутся подобные ошибки следования.

Использование итерируемых объектов

Теперь AssetList, Prtfolio, EfficentFrontier и EfficentFrontierReb - это последовательности (sequence).

Последовательность формируется из объектов класса ok.Asset.

Можно обращаться к активу по его порядковому номеру. Например, для того чтобы посмотреть текущую цену бумаги:
image

Или использовать в циклах:
image

Версия 1.3.1 - Поддержка Python 3.11. Исправление ошибок

Библиотека теперь работает с Python 3.11.

Исправление ошибок

  • в __init__.py добавлен импорта важных алиасов: symbols_in_namespace, no_dividends_namespaces

Улучшения в документации

В документации классов AssetList, Portfolio, EfficientFrontier, EfficientFrontierReb появились новые методы:

Раньше эти методы не присутствовали на okama.readthedocs.io, т.к. документация классов не показывала наследуемые методы.

Версия 1.3.2 - Поддержка Pandas 2.0.0+

okama теперь работает с Pandas 2.0.0 и более новыми версиями. К сожалению, обратной совместимости нет. okama не будет работать с Pandas 1.5.3 и более ранними релизами.

Новые возможности

  • новые периоды ребалансировки для портфелей (класс Portfolio и классы EfficienFrontier, EfficientFrontierReb): “half-year” and “quarter”. Теперь можно ребалансировать портфели по следующими стратегиям: без ребалансировки, ежемесячная ребалансировка, раз в квартал, раз в полгода, ежегодно
  • новый метод set_values_monthly() для класса Inflation и других макроэкономических классов. Теперь в анализе инфляции можно использовать прогнозные данные, изменять произвольно предыдущие значения
  • dividend_yield_annual новое свойство класса AssetList (ежегодные дивидендные доходности сравниваемых активов)
  • get_dividend_mean_yield() новые метод класса AssetList (средняя дивидендная доходность за выбранный период)
  • метод 'plot_cml() в EfficientFrontier получил аргументy_axe` для переключением между средним геометрическим (CAGR) и средним арифметическим доходности в графиках
  • asset_dividend_yield в in AssetList переименовано в dividend_yield

Подробности о новых методах и свойствах классов можно посмотреть в документации okama.

Версия 1.4.0 - Портфели с изъятиями и пополнениями (DCF)

В версии 1.4.0 сделаны первые шаги по поддержке дисконтированных денежных потоков в инвестиционных портфелях (класс Portfolio). Появилась возможность тестировать инвестиционные стратегии с изъятиями и пополнениями. Метод Монте-Карло позволяет прогнозировать “срок дожития” для портфеля с изъятиями.

Новые возможности

DCF методы для изъятий и пополнений в классе Portfolio

Новые атрибуты класса Portfolio (все новые атрибуты необязательны):

  • initial_amount - размер стартовых инвестиций в Portfolio. Значение FV (на дату last_date)

  • cashflow - размер ежемесячных изъятий/пополнений портфеля. Размер приводится как FV (на дату last_date). Негативные значения - это изъятия из портфеля. Положительные значения - пополнения. Значения денежных потоков дисконтируются ежемесячно на значение discount_rate.

  • discount_rate - значение ставки дисконтирования для расчета PV значений. По умолчания discount_rate равна None. Если ставка дисконтирования не определена, то значения дисконтируются на размер инфляции. Если данных по инфляции нет, то используется ставка по умолчанию, равная 5% годовых.

Новые методы dcf. в классе Portfolio

  • dcf.plot_forecast_monte_carlo() метод для отображения на графике прогнозных портфелей с изъятиями/пополнения, сгенерированных методом Монте-Карло. Прогнозные портфели опционально могут отображаться вместе или без тестирования стратегии на исторических данных (см. изображение).

  • dcf.monte_carlo_survival_period() метод для определения “срока дожития” портфеля. Срок дожития – период, когда баланс портфеля остается выше нуля (с учетом изъятий).

  • dcf.wealth_index свойство класса для расчета wealth index (история баланса портфеля). В отличие от Portfolio.wealth_index новое свойство рассчитывает временной ряд балансов с учетом изъятий и пополнений.

  • dcf.survival_period свойство класса для расчета “срока дожития” стратегии с учетом изъятий на исторических данных.

  • dcf.survival_date свойство класса для расчет даты “обнуления” баланса портфеля после серии изъятий.

  • dcf.cashflow_pv свойство класса для расчета дисконтированной величины ежемесячных изъятий (PV) на момент начала исторического периода (first_date).

  • dcf.initial_amount_pv свойство класс для расчета дисконтированной величины стартовых инвестиций на момент начала исторического периода (first_date).

Новые свойства класса Portfolio

  • assets_dividend_yield свойство класса для расчета дивидендной доходности за последние 12 месяцев для каждого из активов. Возвращается временной ряд доходностей за рассматриваемый промежуток дат.

  • dividends_annual свойство класса, возвращающее временной ряд суммы дивидендов за календарный год каждого из активов.

Новые методы и свойства класса AssetList

  • get_rolling_risk_annual() метод для расчета скользящего риска (стандартного отклонения доходности) для заданной длины окна в месяцах.

  • get_dividend_mean_yield()метод для расчета среднего арифметического годовой дивидендной доходности (LTM) за указанный период.

  • dividend_yield_annual свойство класса для расчета дивидендной доходности (LTM) на конец каждого календарного года.

Изменения в существующих методах и свойствах

  • Asset_List.risk_annual возвращает временной ряд риска, приведенного к годовым значениям (раньше возвращало значение на конец периода).

  • Portfolio.recovery_period возвращает временной ряд периодов восстановления активов после падания (раньше возвращало самый длинный период).

  • describe() метод дополнительно показывает среднее арифметическое доходности актива, приведенное к годовым значениям (классы Portfolio и AssetList).

  • новый аргумент xy_text в методе plot_assets() позволяет настроить смещение текстовых меток относительно отображаемой на графике точки с доходностью и риском актива (классы Portofolio, AssetList).

Новые примеры в формате Jupyter Notebooks

  • 04 investment portfolios with DCF.ipynb примеры инвестиционных портфелей с пополнениями / изъятиями (класс Portofolio). Применение метода Монте-Карло для прогнозирования сроков дожития портфелей с изъятиями.

  • 05 macroeconomics - inflation rates.ipynb примеры работы с классами макроэкономических данных: Inflation, Rate и Ratio. Инфляция, ставки центробанков стран мира, CAPE 10 Шиллера.

Исправление ошибок

  • Дубликаты тикеров больше не разрешены в списках активов и автоматически убираются, если это возможно. Актуально для классов: AssetList, Portfolio, EfficientFrontier, EfficientFrontierReb

Подробности о новых методах и свойствах классов можно посмотреть в документации okama.

Самое любопытное из релиза okama 1.4.0

В релизе 1.4.0 собрано довольно много нового. Всего не покажешь, да и зачем, если есть новые блокноты Jupyter Notebook с примерами. Но самое интересно, всё-таки показать стоит.

Изъятия и пополнения портфеля. Дисконтирование (DCF)

Самое прогрессивное изменение – включение в инвестиционные стратегии (класс Portfolio) возможности изъятия и пополнения. Другие метрики портфеля, такие как риск и доходность предполагают, что промежуточных денежных потоков нет. Поэтому новые методы и свойства класса пришлось изолировать. Теперь они доступны через конструкцию типа Portfolio.dcf.wealth_index.

Пример

Создаём консервативную инвестиционную стратегию из облигаций, акций и золота (60/30/10). Пенсионные накопления равны 10 млн. руб. (пусть это будет пенсионный портфель). Будущий пенсионер планирует ежемесячно снимать 40 тыс. руб. в качестве прибавки к пенсии. На сколько хватит сбережений? Иными словами – какой “срок дожития” в этой стратегии?

Создаем портфель:


discount_rate здесь умышлено равно None. Это значит, что мы соглашаемся дисконтировать изъятия на среднюю инфляцию. Для большинства это ОК. Если кто-то хорошо разбирается в DCF, то можно поставить свою ставку дисконтирования.

Ребалансировка портфеля делает один раз в год.

Для удобства портфелю присвоен символ symbol="retirement_portf.PF". Теперь на графиках портфель будет обозначаться этим символом.

Теперь можно протестировать стратегию вместе с изъятиями на исторических данных.


На графике видно, что баланс портфеля продержался выше нуля весь срок (21 год). Портфелю не удалось опередить инфляцию, хотя он держался долго - примерно до 2020 года. Но потом изъятия во время просадки его “подкосили”.

Прежде чем принимать какие-то инвестиционные решения, важно протестировать стратегию не только на исторических данных, но и спрогнозировать… На сколько денег хватит!? Тот факт, что портфель в прошлом продержался 21 год, не значит, что во всех случаях ему это удастся в будущем.

Дальше стоит побаловаться с методом Монте-Карло. Посмотрим, как портфель “выживает” на случайно сгенерированных данных доходности. Параметры генерации доходностей тем не менее совпадают с историческими параметрами стратегии.


Здесь мы сгенерировали 50 случайных сценариев. Прогноз сделан на 30 лет. Старт инвестиций произошел в начале периода (backtest=True) в 2003 году.
По графику видно, что большинство случайных сценариев “обнулили” портфель где-то между 2034 и 2050 годами. Но были и оптимистичные сценарии, когда через 30 лет баланс портфеля всё еще не был равен нулю. Не стоить забывать, что каждый месяц размер изъятий из портфеля индексируется на инфляцию.

Итак, всё выглядит довольно оптимистично… Но хорошо бы получить количественные оценки. На графике это сделать сложно.

Для этого будем генерировать не 100 сценариев а 1000. И прогноз будет не на 30 лет а на 50.


(Google Colab считал это 6 минут. Не ждите быстрых результатов)

Метод dcf.monte_carlo_survival_period() тоже генерирует случайные сценарии (выбрано нормальное распределение, но можно использовать логнормальное). Но в данном случае ничего рисовать не нужно. Это просто распределение “периодов дожития” во всех случайных сценариях.
Свойства этого распределения замечательно отображает функция Pandas describe().

Как видно, в среднем “срок дожития” стратегии составляет 29 лет.
25% перцентиль (пессимистичные сценарии) - 20 лет.
25 перцентиль (оптимистичные сценарии) - 37 лет.

Что очень неплохо для пенсионного портфеля.

П.С. Это не инвестиционная рекомендация. Не надо применять такой портфель :slight_smile: При помощи okama можно подобрать веса лучше и диверсифицировать портфель внутри каждого из классов.

Продолжаем знакомиться с новшествами версии 1.4.0…

Смотрим внимательней на риск

Инвесторов традиционно больше интересует потенциальная доходность. Риск тоже… но только во вторую очередь. Такого “крена” не удалось избежать и в окаме. По доходности есть масса метрик. Многие из них - с возможностью построения скользящих для наблюдением доходности на разных промежутках времени. А риск… он и есть риск. В окаме существует несколько метрик риска (стандартное отклонение, VaR, CVaR, полудисперсия и т.п.). Но все они за полный промежуток исторических данных. Не было никаких скользящих. В качестве эксперимента в версии 1.4.0 мы начали исправлять этот “перекос”. Пока только для класса AssetList

Теперь Asset_List.risk_annual возвращает не число, а временной ряд стандартного отклонения доходности, приведенного к годовым значениям.

Кроме того появился новый метод get_rolling_risk_annual() для получения скользящего риска с настраиваемым размером окна.

Пример

Создаём список активов из акций и облигаций США. У этого списка история с 1995 года.

al = ok.AssetList(["DJI.INDX", "SP500BDT.INDX"], inflation=True)

Теперь посмотрим, как менялся риск со временем:

al.risk_annual.plot()


Каждая точка графика отображает суммарную историю с начала периода наблюдений. Невооруженным глазом видно, что риск - довольно постоянная величина. У акций он менялся в пределах 15-17%. У облигаций от 5 до 6%. Это очень узкие рамки! Попробуйте посмотреть, как прыгала доходность… ничего подобного вы не увидите. Риск (стандартное отклонение доходности) - довольно мало меняется со временем.

Но может быть это правило “сломалось” в последние годы? Полная история наблюдений не поможет проверить. Если в последние годы и были изменения, они “утонут” в статистике предыдущих десятилетий.

Посмотрим долгосрочные 10-летние скользящие риска:

al.get_rolling_risk_annual(window=12 * 10).plot()


Теперь в каждой точке графика мы видим только статистику за предыдущие 10 лет (а не за весь период).
Но вывод всё тот же… риск меняется в очень узких (по сравнению с доходностью) промежутках.

П.С. Portfolio.risk_annual тоже возвращает временной ряд. А вот get_rolling_risk_annual еще не прикрутили. Это задача ближайшего релиза.

Ещё один пример из полезностей версии 1.4.0… видимо, уже последний

Как быстро восстанавливается портфель после просадок?

Когда работаешь с реальными инвестиционными стратегиями, рано или поздно возникает важный вопрос: Как быстро восстанавливается портфель после просадок? Можно получить с виду неплохую стратегию (например, консервативную). У неё будут приемлемые показатели риска/доходности. Но портфель будет долго восстанавливаться после просадок. Что не очень хорошо… особенно для пенсионных портфелей.

С этим помогает разобраться усовершенствованное Portfolio.recovery_period, которое теперь вместо одного числа (максимальный срок) возвращает историю периодов восстановления.

Пример

Возьмем пример консервативного портфеля 60/30/10, который я приводил выше.

История просадок портфеля:


portf.drawdowns.plot()


Было две крупных просадки. Одна во время кризиса недвижимости США. Другая – в последнее время. Визуально можно отметить, что во время кризиса недвижимости портфель восстановился быстрее.

Более точные данные по величине просадок:


portf.drawdowns.nsmallest()

image

Максимальная по глубине просадка произошла в 2008 году. Тогда портфель упал на 23%. В 2022 году размер просадки составил -22%. Это чувствительные снижения, но более или менее приемлемые для портфеля. Особенно, если портфель быстро “отскакивает”.

Проверяем периоды восстановления:


portf.recovery_period.plot(kind='bar')

Сразу посмотрим на данные с цифрами:

portf.recovery_period.nlargest()

image
В 2008 году портфель упал на 23% и восстановился за11 месяцев. В 2022 году история была хуже… Падение составило 22%, но баланс отрос обратно только за 20 месяцев. Это почти 2 года! Для пенсионного портфеля это больше, чем хотелось бы. Обычно во время таких длинных просадок пенсионер чувствует себя очень некомфортно. Деньги снимать необходимо, но делать это очень не хочется. Портфель чувствует себя еще хуже. Когда деньги снимаются во время глубоких просадок, приходится распродавать бумаги по довольно низким ценам. Баланс портфеля с учетом снятий сильно уходит вниз. И это может стать рубежом, после которого восстановить баланс уже не получится. На графике бэктестинга портфеля хорошо заметна эта неприятность в 2022 – 2023.

Вывод

Крайне важно смотреть не только на глубину просадок портфелей. Но и на сроки восстановления (recovery periods). Это особенно важно для стратегий, где предусмотрены изъятия.

В 2022 году длинная просадка связана с тем, что сначала упали акции, а потом к ним присоединились облигации. Облигации всегда снижаются в периоды повышения ставок. Потом акции отыграли падение. Облигации перестали снижаться дальше. Золото тоже помогло, так как в этот период хорошо росла его цена. Но процент золота слишком небольшой, чтобы полностью компенсировать падение.

Этот условный портфель требует дальнейшей модернизации. Прежде всего – более глубокой диверсификации облигаций. Индекс ОФЗ RGBITR, который занимает 60% портфеля, имеет длинную дюрацию (в состав индекса входят довольно длинные бумаги). Поэтому RGBITR и очень волатилен. Его лучше “разбавить” другими видами облигаций.

Кроме того, пропорция 60/30/20 может рассматриваться только как “учебная”. Она далека от оптимальной для консервативного портфеля.

3 сообщения были перемещены в эту тему: Современна теория портфеля. Распределение активов