Optimally rebalancing a portfolio without selling ⚖️

Jakub Svehla—Dec 26, 2019

The other day, I was trying to figure out how to optimally rebalance my portfolio without selling any shares that I am currently holding. I hoped there would be some Google Sheets formula to solve the problem, so the first thing I tried was googling for some solution but I could not find anything at all. While there are tons of resources on how to rebalance a portfolio according to asset allocation, they usually assume that you can sell your current shares or short them. However, due to tax implications, I would like to avoid that. So I decided to sit down and figure out a solution myself. Since it might be useful to others as well I decided to write down the process of my arriving at the final solution step by step.

In this article, we will first look into how to optimally allocate money in a new portfolio defined by asset allocation and then how to optimally rebalance it with only buying additional shares (without selling the overweighted ones) when it drifts. Note that by rebalancing by only buying new shares you won't be able to always rebalance it perfectly with a limited budget, but that's okay with me.

Also, note that we are not optimizing the asset allocation itself, that's another optimization problem. We are trying to buy the shares in such a way that we follow a given asset allocation as closely as possible.

Example

As an example, assume that we started investing in 2013 and we decided that we will invest \(\$10\,000\) every year. We want to have \(60\%\) in global stock markets and \(40\%\) in global bonds, which we will achieve by buying Vanguard Total World Stock ETF (VT) and Vanguard Total Bond Market ETF (BND), respectively.

Let's take a look at how many shares of each fund we would have to buy each year to keep the target asset allocation.

Year 1

At the beginning of 2013, VT and BND fonds cost \(\$50.22\) and \(\$83.78\), respectively. We don't have any shares yet. How many of each do we have to buy to have 60/40 asset allocation?

Given the budget, our goal is to buy as many shares as possible and at the same time minimize the deviation from the target allocation. The deviation from the target allocation, also called a portfolio drift, is defined as the total absolute deviation of each asset (class) from its target allocation weight divided by two, i.e.

\[ \begin{aligned} \frac{1}{2} \sum_i^n \left| \frac{x_i m_i}{\sum_j^n x_j m_j} - t_i \right| \\= \frac{1}{2} \left\| \frac{x \circ m}{x^Tm} - t \right\|_1 \text{,} \end{aligned} \]

where \(x \in \mathbb{N}_0^n\) is a vector of numbers of assets of each asset class \(i = 1, 2, \dots, n\), \(m \in \mathbb{R}_+^n\) is a vector of their current market prices, \(t \in \mathbb{R}_+^n\) is a vector of target allocation weights and \(n \in \mathbb{N}\) is a total number of asset classes we want to invest in.

For example, if we end up with \(70\%\) of stocks and \(30\%\) of bonds, instead of \(60\%\) and \(30\%\), the total drift of the portfolio will be \(10\%\).

Now when we know how to quantify/calculate the deviation we can formulate our goal as a mathematical optimization problem. Given a budget \(b \in \mathbb{R}_+^N\) we want to buy \(x\) assets of each asset class so that we

\[ \begin{array}{rl} \text{minimize} & \frac{1}{2} \left\| \frac{x \circ m}{b} - t \right\|_1 \\ \text{subject to} & x^T m \le b \text{,} \\ & b - x^T m \lt \min_i m_i \text{.} \end{array} \]

In other words, we would like to minimize the portfolio drift (defined above) while we cannot spend more than the budget (we cannot short) and we want to spend it all, i.e. the amount left is lower than a price of the cheapest asset. It is also important to note that we can buy only whole shares of each asset, hence \(x\) is a vector of non-negative integers, \(x \in \mathbb{N}_0^N\).

This problem is an instance of a mixed-integer constrained least absolute deviations problem. Since it is a convex optimization problem, we can use CVXPY, which is a convex optimization library in Python, to solve the problem.

%%capture
! pip install numpy cvxpy
import numpy as np
import cvxpy as cp
budget = 10_000

market_prices = np.array([50.22, 83.78])
target_allocation_weights = np.array([0.6, 0.4])
x = cp.Variable(len(target_allocation_weights), integer=True)

asset_class_values = cp.multiply(x, market_prices)
total_value = budget
actual_allocation_weights = asset_class_values / total_value
portfolio_drift = cp.norm1(actual_allocation_weights - target_allocation_weights) / 2

objective = cp.Minimize(portfolio_drift)

total_cost = x @ market_prices

constraints = [
    x >= 0,
    budget >= total_cost,
    (budget - total_cost) <= market_prices.min() - 1e-2,
]

prob = cp.Problem(objective, constraints)

prob.solve()
# prob.status

positions = np.round(x.value).astype(np.int)

positions
  array([119,  48])
np.round(positions * market_prices / (positions @ market_prices), 2)
  array([0.6, 0.4])

You can see that we need to buy 119 shares of VT and 48 shares of BND, which indeed corresponds to \(60\%\) of stocks and \(40\%\) of bonds.

You might have noticed that in the constraints we are saying that \(b - x^T m \le \min_i m_i - 0.01\) instead of \(b - x^T m \lt \min_i m_i\). In other words, we are saying that we have to be at least one cent shy of the minimum asset price. We have to do this because CVXPY does not allow strict inequalities in constraints.

Year 2

In 2014, we want to again invest \(\$10\,000\) in our portfolio. However, let's first see how much the portfolio drifted from the target allocation during 2013. Prices of VT and BND are now \(\$59\) and \(\$80.1\). We can see that VT went up by \(17\%\) and BND down by \(4\%\).

market_prices = np.array([59. , 80.1])
asset_class_values = positions * market_prices
total_value = positions @ market_prices
actual_allocation_weights = asset_class_values / total_value
portfolio_drift = np.linalg.norm(actual_allocation_weights - target_allocation_weights, ord=1) / 2

portfolio_drift.round(2)
  0.05

We can see that during the first year the portfolio drifted by almost \(5\%\). We would like to restore it to its target allocation just by buying new assets and avoid selling. We would like to buy new assets and rebalance the portfolio without selling any existing assets. Rebalancing is a process of restoring your portfolio to its target allocation after it drifted. If your asset allocation drifts the risk might increase but not necessarily the expected return. Hence, it’s used to minimize risk after you chose your desired allocation and also to stay the desired course.

This time, when minimizing the deviation from the target allocation, we need to take into account both the already held assets and the newly bought ones. We can calculate the portfolio drift analogically to the first case but we have to add the value of the current positions as well. So the drift can be calculated as

\[ \frac{1}{2} \left\| \frac{a \circ m + x \circ m}{a^Tm + b} - t \right\|_1 \text{,} \]

where \(a \in \mathbb{N}_0^n\) is a vector of current asset positions.

We get the following optimization problem:

\[ \begin{array}{rl} \text{minimize} & \frac{1}{2} \left\| \frac{a \circ m + x \circ m}{a^Tm + b} - t \right\|_1 \\ \text{subject to} & x^T m \le b \text{,} \\ & b - x^T m \lt \min_i m_i \text{.}\end{array} \]

which can be solved analogically to the previous case. The only thing that changed is the calculation of the portfolio drift inside the objective function.

asset_class_values = cp.multiply(positions, market_prices) + cp.multiply(x, market_prices)
total_value = positions @ market_prices + budget
portfolio_drift = cp.norm1(asset_class_values / total_value - target_allocation_weights) / 2

objective = cp.Minimize(portfolio_drift)

prob = cp.Problem(objective, constraints)

prob.solve()

new_positions = np.round(x.value).astype(np.int)
positions += new_positions

new_positions
  array([107,  55])

As we can see, we should buy 107 shares of VT and 55 shares of BND in order to restore the desired allocation.

positions
  array([214, 110])
np.round(positions * market_prices / (positions @ market_prices), 2)
  array([0.59, 0.41])

After that, we would end up with \(59\%\) in stocks and \(41\%\) in bonds.

If you have any comments, feedback or questions, let me know on Twitter or via email.


Thanks for reading

If you liked the post, subscribe to my newsletter to get new posts in your mailbox! 📬

Also, feel free to buy me a coffee. ☕️