Last active
June 18, 2024 22:26
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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