Skip to content

Instantly share code, notes, and snippets.

@rileypeterson
Last active June 18, 2024 22:26
Show Gist options
  • Save rileypeterson/74dc45a49c204a12b60bbe0b24d434a0 to your computer and use it in GitHub Desktop.
Save rileypeterson/74dc45a49c204a12b60bbe0b24d434a0 to your computer and use it in GitHub Desktop.
Code to rebalance a portfolio of assets with the limitations of buying a minimum dollar amount and not being able to sell. Not sure if this will always work, but seems to do pretty good for the most part. It was crazy to me I couldn't find something like this online.
import numpy as np
from scipy.optimize import minimize
def no_sell_reallocate(x, t, desired_spend=None, names=None):
"""
Optimally (I hope so) realign the current values of investments x to some
portfolio target allocation t, with the limitation that you cannot reduce the
amount of any current investment (i.e. you can't sell).
Parameters
----------
x : array-like
Current dollar amounts of each asset allocation.
t : array-like
The desired target allocation. Expressed as a decimal between [0, 1].
desired_spend : None | int | float, optional
The amount of money you want to invest, if None then the minimum amount is spent
in order to re-balance the portfolio.
names : None | list[str], optional
Names of assets, in same order as x and t.
Returns
-------
np.array
The amount to buy of each asset, in order to reach the target allocation (or get as close as possible).
"""
assert len(x) == len(t), "Number of current assets must equal number of targets"
assert all(i <= 1 for i in t), "Targets should be less than or equal to 1"
assert sum(t) == 1, "Asset allocation targets should sum to 1"
x = np.array(x)
t = np.array(t)
s0 = sum(x)
nan_t = t.copy()
nan_t[t == 0] = np.nan
div = x / nan_t
print(div)
s = np.nanmax(div) + np.sum(x[t == 0])
b = np.array([s0] + [-j for j in x])
if desired_spend is None:
desired_spend = np.nanmax(div) - s0
if desired_spend and desired_spend + s0 < s:
s = desired_spend + s0
a = np.eye(len(x) + 1)
a[0, 1:] = -np.ones(len(x))
a[1:, 0] = -np.array(t)
a[0, 1:] = 0
b[0] = s
a = np.vstack((a, [1] + (a.shape[1] - 1) * [-1]))
b = np.append(b, s0)
print(a)
print(b)
np.linalg.norm(np.dot(a, np.zeros(a.shape[1])) - b)
fun = lambda y: np.linalg.norm(np.dot(a, y) - b)
bounds = [(0.0, max(s0 + (desired_spend or 0), s))] + [
(0.0, None) for _ in range(a.shape[1] - 1)
]
out = minimize(
fun,
np.zeros(a.shape[1]),
method="L-BFGS-B",
bounds=bounds,
)
print(out)
out = out.x
print(f"Current Account Total: ${sum(x):.4f}")
out = np.delete(out, 0)
inc_s = round(sum(out), 2)
inc_n = (desired_spend or 0) - inc_s
amt_tots = []
for i, amt in enumerate(out):
amt_tot = round(amt + round(t[i] * inc_n, 2), 2)
s = f"x{i} Amount to Buy: ${amt_tot:.2f}"
if names:
s = f"{names[i]} Amount to Buy: ${amt_tot:.2f}"
print(s)
amt_tots.append(amt_tot)
print(f"Sum of buys: {sum(amt_tots):.2f}")
amt_tots = np.array(amt_tots)
new_alloc_amounts = amt_tots + x
new_allocs = new_alloc_amounts / sum(new_alloc_amounts)
print(f"New Allocation: {new_allocs}")
print(f"New Allocation Amounts: {new_alloc_amounts}")
print(f"New Account Total: ${sum(new_alloc_amounts):.2f}")
return out.round(2)
if __name__ == "__main__":
# Example with 4 asset classes (e.g. mutual funds)
# Large cap, international, small cap, cash
target_allocations = [0.5, 0.25, 0.2, 0.05]
current_amounts = [
5010,
2400.32,
2030.10,
500,
]
names = [
"Large Cap",
"International",
"Small Cap",
"MM/Cash",
]
no_sell_reallocate(
current_amounts, target_allocations, desired_spend=1000, names=names
)
Current Account Total: $9940.4200
Large Cap Amount to Buy: $460.21
International Amount to Buy: $334.79
Small Cap Amount to Buy: $157.98
MM/Cash Amount to Buy: $47.02
Sum of buys: 1000.00
New Allocation: [0.5 0.25000046 0.19999963 0.04999991]
New Allocation Amounts: [5470.21 2735.11 2188.08 547.02]
New Account Total: $10940.42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment