Okama 1.4.0 - Новая версия финансовой библиотеки для Python. Портфели с пополнениями и изъятиями

01 марта 2024

Вышел новый релиз финансовой библиотеки okama. Пора обновляться до последней версии:

pip install okama -U

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

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

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

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

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

Пример

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

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

weights = [.6, .3, .1]
portf = ok.Portfolio(
    ["RGBITR.INDX", "MCFTR.INDX", "GC.COMM"],
    ccy="RUB",
    weights=weights,
    inflation=True,
    symbol="retirement_portf.PF",
    rebalancing_period="year",
    cashflow=-50_000,
    initial_amount=10_000_000,
    discount_rate=None,
)
portf
symbol                               retirement_portf.PF
assets                [RGBITR.INDX, MCFTR.INDX, GC.COMM]
weights                                  [0.6, 0.3, 0.1]
rebalancing_period                                  year
currency                                             RUB
inflation                                       RUB.INFL
first_date                                       2003-01
last_date                                        2024-02
period_length                         21 years, 2 months
dtype: object

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

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

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

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

portf.dcf.wealth_index.plot()

Тестирование консервативного инвестиционного портфеля с изъятиями на исторических данных. Okama

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

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

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

portf.dcf.plot_forecast_monte_carlo(distr="norm", years=30, backtest=True, n=100)

Прогнозирование баланса портфеля с изъятиями методом Монте-Карло. Okama

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

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

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

s = portf.dcf.monte_carlo_survival_period(distr="norm", years=50, n=1000)
s.describe()
count    1000.000000
mean       29.210800
std        11.489053
min        10.900000
25%        20.000000
50%        25.450000
75%        37.000000
max        50.000000
dtype: float64

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

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

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

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

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

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

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

Пример

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

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

portf.drawdowns.plot()

История просадок инвестиционного портфеля. Okama

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

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

portf.drawdowns.nsmallest()
date
2008-11   -0.239314
2008-10   -0.235322
2022-02   -0.226408
2008-12   -0.217975
2022-09   -0.210038
Freq: M, Name: retirement_portf.PF, dtype: float64

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

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

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

История периодов восстановления портфеля после просадок. Okama

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

portf.recovery_period.nlargest()
portf.recovery_period.nlargest()
date
2023-06    20
2009-05    11
2013-10     8
2014-06     7
2004-10     6
Freq: M, Name: retirement_portf.PF, dtype: int64

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

Вывод

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

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

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

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

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

Инвесторов традиционно больше интересует потенциальная доходность. Риск тоже… но только во вторую очередь. Такого “крена” не удалось избежать и в окаме. По доходности есть масса метрик. Многие из них - с возможностью построения скользящих для наблюдением доходности на разных промежутках времени. А риск… он и есть риск. В окаме существует несколько метрик риска (стандартное отклонение, 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()

История изменения риска (стандартного отклонения доходности) портфеля. Okama

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

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

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

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

Скользящий 10-летний риск (стандартное отклонение доходности) инвестиционного портфеля. Okama

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

Подробно о нововведениях в okama 1.4.0

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

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

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

  • plot_forecast_monte_carlo() метод для отображения на графике прогнозных портфелей с изъятиями/пополнения, сгенерированных методом Монте-Карло. Прогнозные портфели опционально могут отображаться вместе или без тестирования стратегии на исторических данных (см. изображение).
  • monte_carlo_survival_period() метод для определения “срока дожития” портфеля. Срок дожития – период, когда баланс портфеля остается выше нуля (с учетом изъятий).
  • wealth_index свойство класса для расчета wealth index (история баланса портфеля). В отличие от Portfolio.wealth_index новое свойство рассчитывает временной ряд балансов с учетом изъятий и пополнений.
  • survival_period свойство класса для расчета “срока дожития” стратегии с учетом изъятий на исторических данных.
  • survival_date свойство класса для расчет даты “обнуления” баланса портфеля после серии изъятий.
  • cashflow_pv свойство класса для расчета дисконтированной величины ежемесячных изъятий (PV) на момент начала исторического периода (first_date).
  • 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) на конец каждого календарного года.

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

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

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

  • 04 investment portfolios with DCF.ipynb примеры инвестиционных портфелей с пополнениями / изъятиями (класс Portofolio). Применение метода Монте-Карло для прогнозирования сроков дожития портфелей с изъятиями.
  • 05 macroeconomics - inflation rates.ipynb примеры работы с классами макроэкономических данных: Inflation, Rate и Ratio. Инфляция, ставки центробанков стран мира, CAPE 10 Шиллера.

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

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

Документация okama

В документацию включены все новые методы и свойства классов. Добавлены описания и примеры для ранее существовавших методов.

Документация okama на Readthedocs

Open in Colab

Блокнот с примерами расчетов из статьи в формате Jupyter Notebook доступен на Google Colab.

В блокноте воспроизведены подобные расчеты для валют и инфляций других стран (США, Евросоюз и др.).

Теги: okama Python DCF Риск

Комментарии ()

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

    наверх