Ребалансировка — это важная часть долгосрочного управления портфелем. По мере изменения цен активов ваш инвестиционный портфель может отклоняться от целевого распределения, что потенциально увеличивает риск и может снизить доходность.
В этой статье я покажу, как контролировать веса активов в портфеле с помощью различных стратегий ребалансировки, используя библиотеку okama для Python.
Что такое ребалансировка?
Ребалансировка — это процесс восстановления целевого соотношения ценных бумаг в портфеле за счет продажи и покупки. После ребалансировки все активы возвращаются к своим исходным (целевым) весам.
okama поддерживает несколько стратегий ребалансировки инвестиционного портфеля. Эти стратегии делятся на два типа:
Календарная ребалансировка
Ребалансировка по условию
Календарная ребалансировка
Календарная ребалансировка — это стратегия, которая предполагает восстановление пропорций ценных бумаг через регулярные промежутки времени, например, ежемесячно, ежеквартально или ежегодно.
В окаме доступные следующие периоды для календарных стратегий ребалансировки:
'month' (месяц)
'quarter' (квартал)
'half-year' (полгода)
'year' (год)
Ребалансировка по условию (rebalancing bands)
Стратегия ребалансировки по условию определяется максимальным абсолютным или относительным отклонением.
1. Абсолютное отклонение
Измеряет простую разницу (в процентных пунктах) между текущим и целевым распределением.
Формула:
Пример:
Цель: 40% облигаций
Текущий вес: 45% облигаций
Абсолютное отклонение: +5% (45% - 40%)
Когда использовать?
✔ Лучше всего подходит для ценных бумаг с большим весом (например, 60/40 акции/облигации)
2. Относительное отклонение
Измеряет отклонение в процентах от целевого веса.
Формула:
Пример:
Цель: 10% золота
Текущий вес: 12% золота
Относительное отклонение: +20% ( 12%-10% / 10% )
Когда использовать?
✔ Лучше подходит для небольших долей (например, изменение на 2% при целевом весе 10% = 20% относительного изменения)
Ключевые различия
Сценарий | Абсолютное отклонение | Относительное отклонение |
|---|---|---|
Цель 50%, Текущий 55% | +5% | +10% |
Цель 10%, Текущий 12% | +2% | +20% |
Без ребалансировки ("Купи и держи")
Если инвестиционная стратегия не включает ребалансировку, веса активов в портфеле могут произвольно отклоняться от исходных целевых значений.
Почему стоит использовать Okama?
Okama — это библиотека с открытым исходным кодом для анализа инвестиционных портфелей со следующими возможностями
Анализ ценных бумаг и портфелей
Прогнозирование с помощью Монте-Карло
Использование различных видов распределений (нормальное, логнормальное, Стьюдента и т.п.)
Оптимизация и построение Границы эффектвности
Визуализации
Все подходы соответствуют рекомендациям CFA
Бесплатная база исторических данных бирж мира, валют, макростатистики
Подробное описание библиотеки Okama можно найти в предыдущей статье.
Перейдем к практике...
Настройка
Сначала установите Okama через pip:
pip install okama
Импорт пакетов:
import warnings warnings.filterwarnings("ignore") import matplotlib.pyplot as plt import okama as ok
Настройка стратегии ребалансировки
Класс Rebalance используется для настройки стратегий ребалансировки.
Базовая стратегия ребалансировки портфеля на основе календаря (выполняемая ежегодно) может быть настроена следующим образом:
# настройка стратегии ребалансировки на основе календаря с периодом 1 год reb_calendar = ok.Rebalance(period="year")
Okama работает с ежемесячными рядами данных. Если период ребалансировки равен месяцу, мы имеем классическую ситуацию Марковица с постоянно ребалансируемым портфелем, в котором веса не отклоняются от целевых значений.
reb_calendar_month = ok.Rebalance( period="month", abs_deviation=None, rel_deviation=None )
То же самое для стратегии ребалансировки по условию с абсолютным отклонением 5%:
reb_bands_abs = ok.Rebalance(abs_deviation=0.05)
Стратегия с относительным отклонением 10%:
reb_bands_rel = ok.Rebalance(rel_deviation=0.10)
Все стратегии ребалансировки можно комбинировать:
reb = ok.Rebalance(period="half-year", abs_deviation=0.15, rel_deviation=0.10) reb
Вывод:
period half-year abs_deviation 0.15000 rel_deviation 0.10000 dtype: object
В гибридных стратегиях применяется календарный принцип (например, раз в год). Но ребалансировка запускается только в том случае, если отклонение в весе одного из активов превышает установленное условие.
Чтобы настроить стратегию без ребалансировки:
no_reb = ok.Rebalance(period="none") no_reb
Вывод:
period none abs_deviation None rel_deviation None dtype: object
Стратегии ребалансировки в Portfolio
Все стратегии ребалансировки доступны в классе Portfolio okama и могут быть использованы для бэктестинга или прогнозирования. В okama все ребалансировки происходят в конце периода (последний день месяца, последний месяц года и т. д.).
Портфель 60/40
Как выглядит "идеальный портфель"? С точки зрения контроля рисков, в "идеальном" портфеле пропорции активов должны быть постоянными.
Давайте создадим простой портфель 60/40 с популярными облигациями и ETF-фондами акций и ежемесячной ребалансировкой (постоянные веса).
target_weights = [0.60, 0.40] pf2 = ok.Portfolio( ["AGG.US", "SPY.US"], weights=target_weights, last_date="2025-06", ccy="USD", rebalancing_strategy=reb_calendar_month, # веса постоянны inflation=False,) pf2
Вывод:
symbol portfolio_8100.PF assets [AGG.US, SPY.US] weights [0.6, 0.4] rebalancing_period month rebalancing_abs_deviation None rebalancing_rel_deviation None currency USD inflation None first_date 2003-10 last_date 2025-06 period_length 21 years, 9 months dtype: object
Свойство Portfolio.weights_ts показывает, как изменялись веса портфеля в прошлом. Для этого набора ценных бумаг доступны исторические данные с глубиной 21 год.
weights_no_rebalancing = pf2.weights_ts weights_no_rebalancing.plot();

Так выглядят веса активов в классической Современной теории портфеля (Modern Portfolio Theory) Марковица. Однако в реальной жизни держать веса в виде константы невозможно.
Противоположный случай — когда веса меняются без ограничений (портфель без ребалансировки).
# изменить стратегию ребалансировки pf2.rebalancing_strategy = no_reb # построить график весов pf2.weights_ts.plot();

За несколько лет консервативный портфель стал агрессивным по мере роста доли акций.
abs(pf2.weights_ts - target_weights)["SPY.US"].max()
Вывод:
np.float64(0.36386072210732423)
Веса отклоняются от первоначального распределения на 36%. Такое вряд ли понравится кому-то из инвесторов. Попробуем это "вылечить" с помощью стратегий ребалансировки.
Для начала переключим стратегию ребалансировки в существующем портфеле на календарную ребалансировку с частотой 1 год.
pf2.rebalancing_strategy = reb_calendar pf2
Вывод:
symbol portfolio_8100.PF assets [AGG.US, SPY.US] weights [0.6, 0.4] rebalancing_period year rebalancing_abs_deviation None rebalancing_rel_deviation None currency USD inflation None first_date 2003-10 last_date 2025-06 period_length 21 years, 9 months dtype: object
Свойство Portfolio.rebalancing_events показывает события ребалансировки на исторических данных:
ev = pf2.rebalancing_events ev
Вывод:
2003-12 calendar 2004-12 calendar 2005-12 calendar 2006-12 calendar 2007-12 calendar 2008-12 calendar 2009-12 calendar 2010-12 calendar 2011-12 calendar 2012-12 calendar 2013-12 calendar 2014-12 calendar 2015-12 calendar 2016-12 calendar 2017-12 calendar 2018-12 calendar 2019-12 calendar 2020-12 calendar 2021-12 calendar 2022-12 calendar 2023-12 calendar 2024-12 calendar Freq: M, dtype: object
При тестировании было 22 ребалансировки:
ev.shape
Вывод:
(22,)
Мы можем построить график всех событий, чтобы увидеть, когда происходила ребалансировка:
fig = plt.figure(figsize=(12, 6)) ax = plt.gca() pf2.weights_ts.plot(ax=ax) ax.vlines(x=ev[ev == "calendar"].index, ymin=0, ymax=1, colors="blue", ls="--", lw=1, label="События ребалансировки") ax.set_ylim([0, 1]) ax.legend();

Давайте посмотрим на 5 самых больших отклонений весов:
abs(pf2.weights_ts - target_weights)["SPY.US"].nlargest(n=5)
Вывод:
2008-12 0.11913 2008-11 0.10794 2008-10 0.08644 2013-12 0.07366 2021-12 0.06630 Freq: M, Name: SPY.US, dtype: float64
Максимальное отклонение веса активов произошло в 2008 году во время финансового кризиса и достигло почти 12%. Это было значительно ниже, чем в сценарии без ребалансировки.
Давайте посмотрим, что произойдет, если мы используем вместо календарной ребалансировки стратегию с абсолютным отклонением 5%:
pf2.rebalancing_strategy = reb_bands_abs pf2
Вывод:
symbol portfolio_8100.PF assets [AGG.US, SPY.US] weights [0.6, 0.4] rebalancing_period none rebalancing_abs_deviation 0.05000 rebalancing_rel_deviation None currency USD inflation None first_date 2003-10 last_date 2025-06 period_length 21 years, 9 months dtype: object
Теперь количество событий ребалансировки составляет 13.
ev = pf2.rebalancing_events ev
Вывод:
2005-11 abs 2008-10 abs 2009-01 abs 2009-09 abs 2011-04 abs 2011-09 abs 2012-03 abs 2013-10 abs 2016-11 abs 2018-01 abs 2021-02 abs 2021-12 abs 2024-03 abs Freq: M, dtype: object
ev.shape
Вывод:
(13,)
За исторический период произошло 13 ребалансировок — меньше, чем 22, требуемых при календарной стратегии. В реальной финансовой практике каждая ребалансировка влечет за собой расходы (брокерские комиссии, налоги). Снижения количества ребалансировок может дать существенную экономию...
fig = plt.figure(figsize=(12, 6)) ax = plt.gca() pf2.weights_ts.plot(ax=ax) ax.vlines(x=ev[ev == "abs"].index, ymin=0, ymax=1, colors="blue", ls="--", lw=1, label="Rebalancing events") ax.set_ylim([0, 1]) ax.legend();

abs(pf2.weights_ts - target_weights)["SPY.US"].nlargest(n=5)
Вывод:
2008-10 0.06851 2018-01 0.06004 2021-12 0.05638 2013-10 0.05638 2024-03 0.05448 Freq: M, Name: SPY.US, dtype: float64
Отклонения весов теперь под контролем. Максимальное отклонение снизилось до 6,9% — почти половины от того, что произошло при календарной ребалансировке.
Интересно, что отклонение превышает 5-процентный лимит стратегии. Это происходит потому, что okama проверяет условия ребалансировки в конце месяца (используя месячные данные). В течение одного месяца веса активов могут отклоняться за пределы установленных стратегией лимитов.
Портфель из 3 активов с небольшим распределением
Мы можем протестировать ту же стратегию ребалансировки с абсолютными пределами отклонения веса в портфеле из 3 активов, где один актив (золото) имеет всего 5% распределения.
# установить целевые веса для активов target_weights3 = [0.60, 0.35, 0.05] # установить стратегию ребалансировки rs3 = ok.Rebalance( period="none", abs_deviation=0.10, rel_deviation=None ) # создать портфель с 3 активами pf3 = ok.Portfolio( ["SP500TR.INDX", "VBMFX.US", "GC.COMM"], weights=target_weights3, ccy="USD", rebalancing_strategy=rs3, inflation=False,) pf3
Вывод:
symbol portfolio_1335.PF assets [SP500TR.INDX, VBMFX.US, GC.COMM] weights [0.6, 0.35, 0.05] rebalancing_period none rebalancing_abs_deviation 0.10000 rebalancing_rel_deviation None currency USD inflation None first_date 1988-02 last_date 2025-06 period_length 37 years, 5 months dtype: object
Во время бэктестинга условие ребалансировки срабатывало 8 раз.
ev3 = pf3.rebalancing_events ev3
Вывод:
1995-07 abs 1997-07 abs 2002-09 abs 2007-01 abs 2008-10 abs 2013-12 abs 2018-08 abs 2023-06 abs Freq: M, dtype: object
Посмотрим эти события ребалансировки на графике.
fig = plt.figure(figsize=(12, 6)) ax = plt.gca() pf3.weights_ts.plot(ax=ax) ax.vlines(x=ev3.index, ymin=0, ymax=1, colors="blue", ls="--", lw=1, label="Rebalancing by abs") ax.set_ylim([0, 1]) ax.legend();

Были ситуации, когда отклонение для золота было слишком большим. Максимальное отклонение составляет 4,3%, что почти вдвое (!) превышает размер целевого веса для золота.
abs(pf3.weights_ts - target_weights3).max()
Вывод:
Symbols SP500TR.INDX 0.10438 VBMFX.US 0.10640 GC.COMM 0.04257 dtype: float64
В этом случае следует добавить условие относительного отклонения, чтобы лучше контролировать распределение для золота.
pf3.rebalancing_strategy = ok.Rebalance( period="none", abs_deviation=0.10, rel_deviation=0.30 # относительное отклонение 30% )
Количество ребалансировок увеличилось до 14.
ev3 = pf3.rebalancing_events ev3.shape
Вывод:
(14,)
ev3
Вывод:
1989-05 rel 1992-04 rel 1996-06 rel 1997-07 rel 1999-01 rel 2002-07 abs 2006-01 rel 2008-01 rel 2008-12 abs 2011-07 rel 2013-06 rel 2017-10 abs 2021-08 abs 2025-03 rel Freq: M, dtype: object
Теперь у нас есть 10 ребалансировок по относительному условию и только 4 по абсолютному.
Новые события ребалансировки отмечены на графике зеленым цветом.
fig = plt.figure(figsize=(12, 6)) ax = plt.gca() pf3.weights_ts.plot(ax=ax) ax.vlines(x=ev3[ev3 == "abs"].index, ymin=0, ymax=1, colors="blue", ls="--", lw=1, label="Rebalancing by abs") ax.vlines(x=ev3[ev3 == "rel"].index, ymin=0, ymax=1, colors="green", ls="--", lw=1, label="Rebalancing by rel") ax.set_ylim([0, 1]) ax.legend();

Теперь максимальное отклонение золота составляет 2,2%. И это выглядит лучше.
abs(pf3.weights_ts - target_weights3).max()
Symbols SP500TR.INDX 0.11944 VBMFX.US 0.11290 GC.COMM 0.02217 dtype: float64
Мы можем протестировать альтернативный подход: реализовать календарную ребалансировку, которая срабатывает только при превышении пороговых значений абсолютного или относительного отклонения. Этот гибридный метод широко используется на практике, устраняя необходимость постоянного мониторинга весов. Вместо этого инвесторы просто пересматривают свое распределение раз в год (или раз в полгода).
pf3.rebalancing_strategy = ok.Rebalance( period="year", abs_deviation=0.10, rel_deviation=0.20 )
Количество событий ребалансировки увеличилось на 2.
ev3 = pf3.rebalancing_events ev3.shape
Вывод:
(16,)
В таблице событий мы видим, что ребалансировки происходят только в декабре.
ev3
Вывод:
1989-12 rel 1991-12 rel 1995-12 rel 1997-12 rel 1999-12 rel 2001-12 rel 2002-12 rel 2006-12 rel 2007-12 rel 2008-12 abs 2011-12 rel 2013-12 abs 2015-12 rel 2019-12 rel 2021-12 rel 2024-12 rel Freq: M, dtype: object
Большинство из них запускаются относительным условием. Если вы чувствуете себя уверенно при больших отклонениях, количество событий можно уменьшить, настроив параметр относительного отклонения rel_deviation.
ev3[ev3 == "rel"].count()
Вывод:
np.int64(14)
ev3[ev3 == "abs"].count()
Вывод:
np.int64(2)
fig = plt.figure(figsize=(12, 6)) ax = plt.gca() pf3.weights_ts.plot(ax=ax) ax.vlines(x=ev3[ev3 == "abs"].index, ymin=0, ymax=1, colors="blue", ls="--", lw=1, label="Rebalancing by abs") ax.vlines(x=ev3[ev3 == "rel"].index, ymin=0, ymax=1, colors="green", ls="--", lw=1, label="Rebalancing by rel") ax.set_ylim([0, 1]) ax.legend();

На графике мы видим, что было несколько длительных периодов без ребалансировки.
И максимальное отклонение по-прежнему находится под контролем.
abs(pf3.weights_ts - target_weights3).max()
Вывод:
Symbols SP500TR.INDX 0.12670 VBMFX.US 0.11787 GC.COMM 0.02512 dtype: float64
Для пассивного подхода к инвестированию, когда необходимость ребалансировки портфеля проверяется "руками" без автоматически настроенных инструментов, гибридный подход с длительными интервалами (год или полгода) удобен, поскольку устраняет необходимость постоянного мониторинга отклонений весов. С другой стороны, условия по-прежнему не позволяют портфелю слишком сильно отклоняться от намеченной стратегии.
В то же время, чем больше допустимые отклонения веса, тем больше потенциальный бонус от ребалансировки в виде дополнительной доходности.
Бонус за ребалансировку: избыточная доходность
Согласно исследованию Уильяма Бернстайна, ребалансировка позволяет не только контролировать риск, но и может принести дополнительную доходность. Этот эффект возникает, если портфель содержит активы с низкой корреляцией. Библиотека okama позволяет изучить влияние стратегий ребалансировки на эту избыточную доходность и оптимизировать распределение активов. Если вам интересна эта тема, дайте знать в комментариях... Покажу несколько примеров в следующей публикации.
