DEV Community

Cover image for Mastering Investment Portfolio Rebalancing with Python and Okama
Sergey Kikevich
Sergey Kikevich

Posted on

Mastering Investment Portfolio Rebalancing with Python and Okama

Rebalancing is a crucial part of long-term portfolio management. As asset prices fluctuate, your investment portfolio can drift away from its target allocation, potentially increasing risk or reducing expected returns.

In this article, we’ll explore how to control asset weights within a portfolio using various rebalancing strategies using the okama Python library.

What is rebalancing?

Rebalancing is the process by which an investor restores their portfolio to its target allocation by selling and buying assets. After rebalancing all the assets have original (target) weights.

okama supports several investment portfolio rebalancing strategies. These strategies are divided into two types:

  • Calendar-based rebalancing
  • Rebalancing bands (threshold-based)

Calendar-based rebalancing

Calendar-based rebalancing is a strategy that involves adjusting the asset allocation of a portfolio at regular intervals, such as monthly, quarterly or annually.

Available periods for calendar-based strategies:

  • 'month'
  • 'quarter'
  • 'half-year'
  • 'year'

Rebalancing bands (threshold-based)

Rebalancing bands or threshold-based strategy is defined by absolute or relative deviation.

1. Absolute Deviation

Measures the simple difference (in percentage points) between current and target allocation.

Formula:

Absolute Deviation=Current WeightTarget Weight \text{Absolute Deviation} = \text{Current Weight} - \text{Target Weight}

Example:

  • Target: 40% bonds
  • Current: 45% bonds
  • Deviation: +5% (45% - 40%)
When to Use?

✔ Best for fixed rebalancing bands (e.g., "rebalance if ±5% from target")
✔ Simple for balanced portfolios (e.g., 60/40 stocks/bonds)


2. Relative Deviation

Measures deviation as a percentage of the target weight.

Formula:

Relative Deviation=Current Weight - Target WeightTarget Weight×100% \text{Relative Deviation} = \frac{\text{Current Weight - Target Weight}}{\text{Target Weight}} \times 100\%

Example:

  • Target: 10% gold
  • Current: 12% gold
  • Deviation: +20% ( 12%10%10%\frac{12\%-10\%}{10\%} )
When to Use?

✔ Better for small allocations (e.g., 2% shift in 10% target = 20% relative change)
✔ Common in tactical allocation (sector tilts, alternative assets)


Key Comparison

Scenario Absolute Deviation Relative Deviation
Target 50%, Current 55% +5% +10%
Target 10%, Current 12% +2% +20%
  • Absolute: Treats all assets equally (5% shift triggers regardless of target weight)
  • Relative: More sensitive for small allocations (2% shift in 10% target = 20% signal)

No rebalancing ("Buy & hold")

If an investment strategy does not include rebalancing, asset weights in the portfolio may drift arbitrarily from their initial targets.

Why Use Okama?

Okama is an open-source Python library for investment portfolio analysis. It provides tools for:

  • Portfolio construction and analysis
  • Asset allocation and rebalancing
  • Risk and performance metrics
  • Visualization
  • All approaches comply with CFA guidelines

A detailed description of the Okama library can be found in the previous article.
Let’s dive into a practical example.

Setting Up

First, install Okama via pip:

pip install okama
Enter fullscreen mode Exit fullscreen mode

Import packages:

import warnings

warnings.filterwarnings("ignore")

import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = [12.0, 6.0]

import okama as ok

import pandas as pd

pd.options.display.float_format = "{:,.5f}".format
Enter fullscreen mode Exit fullscreen mode

Setting a rebalancing strategy

The Rebalance class is used to configure rebalancing strategies.

A basic calendar-based portfolio rebalancing strategy (executed annually) can be configured as follows:

#  set up a calendar-based rebalancing strategy where the period is 1 year
reb_calendar = ok.Rebalance(period="year")  
Enter fullscreen mode Exit fullscreen mode

Okama works with montly data series. If the rebalancing period is equal to a month we have the classic Markowitz situation with always rebalanced portfolio where the weights do not deviate from the target.

reb_calendar_month = ok.Rebalance(
    period="month",  #  set up a calendar-based rebalancing strategy where the period is 1 month
    abs_deviation=None,
    rel_deviation=None
    )
Enter fullscreen mode Exit fullscreen mode

The same for a rebalancing bands (threshold-based) strategy with 5% absolute deviation:

#  set up a rebalancing bands strategy with allowed absolute deviation 5%
reb_bands_abs = ok.Rebalance(
    abs_deviation=0.05
) 
Enter fullscreen mode Exit fullscreen mode

The strategy with relative deviation 10%:

#  set up a rebalancing bands strategy with allowed relative deviation 10%
reb_bands_rel = ok.Rebalance(
    rel_deviation=0.10
)  
Enter fullscreen mode Exit fullscreen mode

All rebalancing strategies can be combined:

reb = ok.Rebalance(period="half-year", abs_deviation=0.15, rel_deviation=0.10)
reb
Enter fullscreen mode Exit fullscreen mode

Output:

period           half-year
abs_deviation      0.15000
rel_deviation      0.10000
dtype: object
Enter fullscreen mode Exit fullscreen mode

In hybrid strategies, a calendar principle is applied (for example, once a year). But rebalancing triggers only if the deviation in weight of one of the assets exceeds the condition. This resembles how it happens in real investor life.

To configure a strategy without rebalancing:

no_reb = ok.Rebalance(period="none")
no_reb
Enter fullscreen mode Exit fullscreen mode

Output:

period           none
abs_deviation    None
rel_deviation    None
dtype: object
Enter fullscreen mode Exit fullscreen mode

Rebalancing strategies in Portfolio

All rebalancing strategies are available in Portfolio class of okama and can be used for backtesting or forecasting. In okama all rebalancings occure in the end of period (the last day of month, last month of year etc.)

The condition for rebalancing bands is verified in the end of every month as okama uses monthly data.

60/40 Portfolio

What does the 'ideal portfolio' look like? From the perspective of risk control, in an 'ideal' portfolio, the proportions of the assets should be constants.

Let's set up a simple 60/40 portfolio with popular bonds and stocks ETFs.

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,  # always rebalanced
    inflation=False,
)
pf2
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Portfolio.weights_ts property shows how the portfolio weights changed in the past. For this set of securities, 21 years of historical data are available.

weights_no_rebalancing = pf2.weights_ts
weights_no_rebalancing.plot();
Enter fullscreen mode Exit fullscreen mode

Assets weights in always rebalanced portfolio

And this is exactly how it has always been presented in the classical Modern Portfolio Theory (MPT) of Markowitz. However, in real live the weights changes.

The opposite case is when the weights change without constraints (not rebalanced portfolio).

# change the rebalancing strategy
pf2.rebalancing_strategy = no_reb
# plot the weights
pf2.weights_ts.plot();
Enter fullscreen mode Exit fullscreen mode

60/40 portfolio weights time series without rebalancing

Over several years, the conservative portfolio became aggressive as stock allocations grew.

abs(pf2.weights_ts - target_weights)["SPY.US"].max()
Enter fullscreen mode Exit fullscreen mode

Output:

np.float64(0.36386072210732423)
Enter fullscreen mode Exit fullscreen mode

The weights deviate 36% from the initial allocation.

We can switch the rebalancing strategy in an existing portfolio to calendar-based rebalancing (annual frequency) and try to keep the risk ander control.

pf2.rebalancing_strategy = reb_calendar
pf2
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Portfolio.rebalancing_events property shows the rebalancing events on the historical data:

ev = pf2.rebalancing_events
ev
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

There were 22 rebalancing events:

ev.shape  # 22 rebalancing events
Enter fullscreen mode Exit fullscreen mode

Output:

(22,)
Enter fullscreen mode Exit fullscreen mode

We can plot all of them to see when rebalances occurred.:

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="Rebalancing events")
ax.set_ylim([0, 1])
ax.legend();
Enter fullscreen mode Exit fullscreen mode

60/40 portfolio calendar rebalancing events

Let's see top 5 weights devations:

abs(pf2.weights_ts - target_weights)["SPY.US"].nlargest(n=5)
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

The maximum asset weight deviation occurred in 2008 during the financial crisis, reaching almost 12%. This was significantly lower than in the non-rebalanced scenario.

Let's see what happens if we set the rebalancing bands with an absolute limit of 5%:

pf2.rebalancing_strategy = reb_bands_abs
pf2
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Now the number of rebalancing events is 13 now.

ev = pf2.rebalancing_events
ev
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode
ev.shape
Enter fullscreen mode Exit fullscreen mode

Output:

(13,)
Enter fullscreen mode Exit fullscreen mode

13 rebalancings occurred during the historical period - fewer than the 22 required under calendar-based rebalancing. In real-world finance, each rebalancing incurs costs (brokerage commissions, taxes). This represents substantial savings...

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();
Enter fullscreen mode Exit fullscreen mode

60/40 portfolio rebalancing events for 5% abs deviation

abs(pf2.weights_ts - target_weights)["SPY.US"].nlargest(n=5)
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Weight deviations are now under control. The maximum deviation reached 6.9% - nearly half of what occurred with calendar rebalancing. Interestingly, this still exceeds the strategy's 5% limit. This occurs because okama checks rebalancing conditions at month-end (using monthly data). Within a single month, asset weights can drift beyond the strategy's prescribed limits.

3 assets Portfolio with small allocation

We can test the same rebalancing strategy with absolute weight deviation limits in a 3-asset portfolio, where one asset (gold) has just a 5% allocation.

# set target weights for the assets
target_weights3 = [0.60, 0.35, 0.05]
# set the rebalancing strategy
rs3 = ok.Rebalance(
    period="none", 
    abs_deviation=0.10, 
    rel_deviation=None
    )

# make a portfolio with 3 assets
pf3 = ok.Portfolio(
    ["SP500TR.INDX", "VBMFX.US", "GC.COMM"],
    weights=target_weights3,
    ccy="USD",
    rebalancing_strategy=rs3,
    inflation=False,
)
pf3
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

During backtesting, the rebalancing condition was triggered 8 times.

ev3 = pf3.rebalancing_events
ev3
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode
ev3.count()
Enter fullscreen mode Exit fullscreen mode

Output:

np.int64(8)
Enter fullscreen mode Exit fullscreen mode

We can see these rebalancing events in the chart.

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();
Enter fullscreen mode Exit fullscreen mode

3 assets portfolio with absolute deviation rebalancing bands

However there were situations when the deviation for gold was large. The max deviation is 4,3% which is almost double (!) from the target allocation.

abs(pf3.weights_ts - target_weights3).max()
Enter fullscreen mode Exit fullscreen mode

Output:

Symbols
SP500TR.INDX   0.10438
VBMFX.US       0.10640
GC.COMM        0.04257
dtype: float64
Enter fullscreen mode Exit fullscreen mode

In this case we should add relative deviation band to control better the allocaction for gold.

pf3.rebalancing_strategy = ok.Rebalance(
    period="none", 
    abs_deviation=0.10, 
    rel_deviation=0.30)
Enter fullscreen mode Exit fullscreen mode

The number of rebalancing increased to 14.

ev3 = pf3.rebalancing_events
ev3.shape
Enter fullscreen mode Exit fullscreen mode

Output:

(14,)
Enter fullscreen mode Exit fullscreen mode
ev3
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Now we have 10 rebalancings by relative band and only 4 by absolute.

ev3[ev3 == "rel"].count()
Enter fullscreen mode Exit fullscreen mode

Output:

np.int64(10)
Enter fullscreen mode Exit fullscreen mode
ev3[ev3 == "abs"].count()
Enter fullscreen mode Exit fullscreen mode

Output:

np.int64(4)
Enter fullscreen mode Exit fullscreen mode

New rebalancing events are market with green color in the chart.

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();
Enter fullscreen mode Exit fullscreen mode

3 assets portfolio with absolute and relative deviation rebalancing bands

Now the max deviation of gold is 2,2%. And it looks better.

abs(pf3.weights_ts - target_weights3).max()
Enter fullscreen mode Exit fullscreen mode
Symbols
SP500TR.INDX   0.11944
VBMFX.US       0.11290
GC.COMM        0.02217
dtype: float64
Enter fullscreen mode Exit fullscreen mode

We can test an alternative approach: implementing calendar-based rebalancing that only triggers when absolute or relative deviation thresholds are breached. This hybrid method is commonly used in practice, eliminating the need for constant weight monitoring. Instead, investors simply review their allocation once a year (or once a half-year).

pf3.rebalancing_strategy = ok.Rebalance(
    period="year", 
    abs_deviation=0.10, 
    rel_deviation=0.20
)
Enter fullscreen mode Exit fullscreen mode

The number of rebalancing events has increase by 2.

ev3 = pf3.rebalancing_events
ev3.shape
Enter fullscreen mode Exit fullscreen mode

Output:

(16,)
Enter fullscreen mode Exit fullscreen mode

In the events table we can see that rebalncings happen only in december.

ev3
Enter fullscreen mode Exit fullscreen mode

Output:

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
Enter fullscreen mode Exit fullscreen mode

Most of them are triggered by the relative band. If one feel safe with larger deviations the number of events can be reduced by adjusting the rel_deviation parameter.

ev3[ev3 == "rel"].count()
Enter fullscreen mode Exit fullscreen mode

Output:

np.int64(14)
Enter fullscreen mode Exit fullscreen mode
ev3[ev3 == "abs"].count()
Enter fullscreen mode Exit fullscreen mode

Output:

np.int64(2)
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode

3 assets portfolio with hybrid rebalancing strategy

In the chart we can see that there were several long periods without rebalancing.

And the max deviation is still under control.

abs(pf3.weights_ts - target_weights3).max()
Enter fullscreen mode Exit fullscreen mode

Output:

Symbols
SP500TR.INDX   0.12670
VBMFX.US       0.11787
GC.COMM        0.02512
dtype: float64
Enter fullscreen mode Exit fullscreen mode

For a passive investment approach, where the need for portfolio rebalancing is checked without automation tools, a hybrid approach with long intervals (a year or half a year) is convenient in one sense, as it eliminates the need to constantly monitor weight deviations. On the other hand, deviation bands still prevent the portfolio from straying too far from the intended strategy.

At the same time, the larger the allowed weight deviations, the greater the potential bonus from rebalancing in the form of additional returns.

The Rebalancing Bonus: Excess Return

According to research by William Bernstein, rebalancing not only provides the obvious benefit of risk control, but can also deliver a considerable excess return. This effect occurs if the portfolio contains assets with low correlation. The okama library allows to study the impact of rebalancing strategies on this excess return and optimize your asset allocation accordingly. If you’re interested in this topic, let me know—I’ll explain how to do this in the next article.

Top comments (0)