Ребалансировка — это важная часть долгосрочного управления портфелем. По мере изменения цен активов ваш инвестиционный портфель может отклоняться от целевого распределения, что потенциально увеличивает риск и может снизить доходность.

В этой статье я покажу, как контролировать веса активов в портфеле с помощью различных стратегий ребалансировки, используя библиотеку okama для Python.

Что такое ребалансировка?

Ребалансировка — это процесс восстановления целевого соотношения ценных бумаг в портфеле за счет продажи и покупки. После ребалансировки все активы возвращаются к своим исходным (целевым) весам.

okama поддерживает несколько стратегий ребалансировки инвестиционного портфеля. Эти стратегии делятся на два типа:

  • Календарная ребалансировка

  • Ребалансировка по условию

Календарная ребалансировка

Календарная ребалансировка — это стратегия, которая предполагает восстановление пропорций ценных бумаг через регулярные промежутки времени, например, ежемесячно, ежеквартально или ежегодно.

В окаме доступные следующие периоды для календарных стратегий ребалансировки:

  • 'month' (месяц)

  • 'quarter' (квартал)

  • 'half-year' (полгода)

  • 'year' (год)

Ребалансировка по условию (rebalancing bands)

Стратегия ребалансировки по условию определяется максимальным абсолютным или относительным отклонением.

1. Абсолютное отклонение

Измеряет простую разницу (в процентных пунктах) между текущим и целевым распределением.

Формула:

\texttt{Абсолютное отклонение} = \texttt{Текущий вес} - \texttt{Целевой вес}

Пример:

  • Цель: 40% облигаций

  • Текущий вес: 45% облигаций

  • Абсолютное отклонение: +5% (45% - 40%)

Когда использовать?

✔ Лучше всего подходит для ценных бумаг с большим весом (например, 60/40 акции/облигации)


2. Относительное отклонение

Измеряет отклонение в процентах от целевого веса.

Формула:

\texttt{Относительное отклонение} = \frac{\texttt{Текущий вес - Целевой вес}}{\texttt{Целевой вес}} \times 100\%

Пример:

  • Цель: 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();
Временной ряд весов портфеля 60/40 без ребалансировки
Временной ряд весов портфеля 60/40 без ребалансировки

За несколько лет консервативный портфель стал агрессивным по мере роста доли акций.

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();
60/40 события ребалансировки портфеля по календарю
60/40 события ребалансировки портфеля по календарю

Давайте посмотрим на 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();
60/40 событий ребалансировки портфеля при отклонении 5% abs
60/40 событий ребалансировки портфеля при отклонении 5% abs
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();
Портфель из 3 активов с ребалансировкой по абсолютному отклонению
Портфель из 3 активов с ребалансировкой по абсолютному отклонению

Были ситуации, когда отклонение для золота было слишком большим. Максимальное отклонение составляет 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();
Портфель из 3 активов с ребалансировкой по абсолютному и относительному отклонению
Портфель из 3 активов с ребалансировкой по абсолютному и относительному отклонению

Теперь максимальное отклонение золота составляет 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();
Портфель из 3 активов с гибридной стратегией ребалансировки
Портфель из 3 активов с гибридной стратегией ребалансировки

На графике мы видим, что было несколько длительных периодов без ребалансировки.

И максимальное отклонение по-прежнему находится под контролем.

abs(pf3.weights_ts - target_weights3).max()

Вывод:

Symbols
SP500TR.INDX   0.12670
VBMFX.US       0.11787
GC.COMM        0.02512
dtype: float64

Для пассивного подхода к инвестированию, когда необходимость ребалансировки портфеля проверяется "руками" без автоматически настроенных инструментов, гибридный подход с длительными интервалами (год или полгода) удобен, поскольку устраняет необходимость постоянного мониторинга отклонений весов. С другой стороны, условия по-прежнему не позволяют портфелю слишком сильно отклоняться от намеченной стратегии.

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

Бонус за ребалансировку: избыточная доходность

Согласно исследованию Уильяма Бернстайна, ребалансировка позволяет не только контролировать риск, но и может принести дополнительную доходность. Этот эффект возникает, если портфель содержит активы с низкой корреляцией. Библиотека okama позволяет изучить влияние стратегий ребалансировки на эту избыточную доходность и оптимизировать распределение активов. Если вам интересна эта тема, дайте знать в комментариях... Покажу несколько примеров в следующей публикации.

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


  1. Maksatiha
    02.07.2025 12:46

    Мозгари, объясните теперь куда мне просто деньги положить, а потом снять с супер-пупер избыточной доходностью? Желательно с карты на карту.


    1. Chilango Автор
      02.07.2025 12:46

      А с чего вы взяли, что этот вопрос к автору именно этой статьи? :)


      1. lczero
        02.07.2025 12:46

        --