PORTFOLIO_REBALANCER
Overview
The PORTFOLIO_REBALANCER
function computes a new allocation of assets across Taxable, Roth, and Traditional accounts to move each asset class closer to its target allocation, while respecting account capacity constraints and prioritizing tax-inefficient assets for tax-advantaged accounts.
This function applies a greedy, rule-based reallocation that respects the order of tax efficiency and account limits. For each asset class, it calculates the difference (delta) between the target allocation and the sum of the current allocations across all accounts. Asset classes are sorted by their tax inefficiency (highest tax score first), so that the most tax-inefficient assets are considered first for placement in tax-advantaged accounts. The function then attempts to add or remove allocations in the order of Roth, Traditional, and Taxable accounts for additions, and Taxable, Traditional, and Roth for reductions, without exceeding account capacities or allowing negative allocations.
This approach does not perform a full optimization or minimize taxes globally, but rather provides a simple, educational example of tax-aware rebalancing. For advanced portfolio optimization, see PyPortfolioOpt on GitHub and the PyPortfolioOpt documentation .
This example function is provided as-is without any representation of accuracy.
Usage
To use the function in Excel:
=PORTFOLIO_REBALANCER(asset_classes, current_alloc, target_alloc, tax_scores, taxable_cap, roth_cap, trad_cap)
asset_classes
(list[list[str]], required): Asset class names as a list of lists, one per asset class.current_alloc
(list[list[float]], required): Current dollar allocations for each asset in each account.target_alloc
(list[list[float]], required): Target total dollar allocation for each asset class.tax_scores
(list[list[float]], required): Tax efficiency score for each asset.taxable_cap
(float, required): Maximum dollars available in the taxable account.roth_cap
(float, required): Maximum dollars available in the Roth account.trad_cap
(float, required): Maximum dollars available in the Traditional account.
The function returns a new allocation for each asset as a 2D list: [[taxable, roth, trad], ...]
. If an error occurs, a string error message is returned.
Examples
Example 1: Rebalancing a Three-Asset Portfolio
This example rebalances a portfolio of three asset classes to their targets, prioritizing tax-inefficient assets for tax-advantaged accounts.
Asset Class | Taxable | Roth | Trad | Target | Tax Score |
---|---|---|---|---|---|
US Stocks | 18000 | 7000 | 5000 | 25000 | 90 |
Bonds | 9000 | 3000 | 8000 | 20000 | 60 |
Cash | 4000 | 2000 | 4000 | 8000 | 30 |
Taxable Cap: 30000
Roth Cap: 8000
Trad Cap: 17000
Excel formula:
=PORTFOLIO_REBALANCER({"US Stocks";"Bonds";"Cash"}, {18000,7000,5000;9000,3000,8000;4000,2000,4000}, {25000;20000;8000}, {90;60;30}, 30000, 8000, 17000)
Expected output:
Asset Class | Taxable | Roth | Trad |
---|---|---|---|
US Stocks | 13000.0 | 7000.0 | 5000.0 |
Bonds | 9000.0 | 3000.0 | 8000.0 |
Cash | 2000.0 | 2000.0 | 4000.0 |
This means the new allocation moves towards the target, reducing US Stocks in the taxable account.
Example 2: Allocating All Assets to Taxable Accounts
This example shows allocation when all assets start in taxable accounts and there is limited capacity in Roth and Trad accounts.
Asset Class | Taxable | Roth | Trad | Target | Tax Score |
---|---|---|---|---|---|
ETF1 | 8000 | 0 | 0 | 9000 | 10 |
ETF2 | 2000 | 0 | 0 | 3000 | 10 |
Taxable Cap: 12000
Roth Cap: 1000
Trad Cap: 1000
Excel formula:
=PORTFOLIO_REBALANCER({"ETF1";"ETF2"}, {8000,0,0;2000,0,0}, {9000;3000}, {10;10}, 12000, 1000, 1000)
Expected output:
Asset Class | Taxable | Roth | Trad |
---|---|---|---|
ETF1 | 8000.0 | 1000.0 | 0.0 |
ETF2 | 2000.0 | 0.0 | 1000.0 |
This means the function has reallocated assets to fill Roth and Trad accounts as much as possible, prioritizing tax efficiency.
Python Code
def portfolio_rebalancer(asset_classes, current_alloc, target_alloc, tax_scores, taxable_cap, roth_cap, trad_cap):
"""
Tax-efficient rebalancing of assets across Taxable, Roth, and Traditional accounts.
Args:
asset_classes: list[list[str]], asset class names as a list of lists, one per asset class.
current_alloc: list[list[float]], current dollar allocations for each asset in each account.
target_alloc: list[list[float]], target total dollar allocation for each asset class.
tax_scores: list[list[float]], tax efficiency score for each asset.
taxable_cap: float, maximum dollars available in the taxable account.
roth_cap: float, maximum dollars available in the Roth account.
trad_cap: float, maximum dollars available in the Traditional account.
Returns:
list[list[float]]: New allocation for each asset as a 2D list: [[taxable, roth, trad], ...]. If an error occurs, a string error message is returned.
This example function is provided as-is without any representation of accuracy.
"""
# --- Input Validation and Data Preparation ---
if not (asset_classes and current_alloc and target_alloc and tax_scores):
return "Error: All input lists must be non-empty."
n = len(asset_classes)
if not (len(current_alloc) == len(target_alloc) == len(tax_scores) == n):
return "Error: All input lists must have the same length."
try:
# Convert all inputs to consistent internal data types
asset_classes = [str(row[0]).strip() for row in asset_classes]
current_alloc = [list(map(float, row)) for row in current_alloc]
target_alloc = [float(row[0]) for row in target_alloc]
tax_scores = [float(row[0]) for row in tax_scores]
# --- Core Rebalancing Logic ---
# Calculate the difference (delta) between target and current total allocation for each asset
deltas = [target_alloc[i] - sum(current_alloc[i]) for i in range(n)]
# Sort assets by tax inefficiency (descending tax score) to prioritize them for tax-advantaged accounts
sorted_idx = sorted(range(n), key=lambda i: -tax_scores[i])
new_alloc = [row[:] for row in current_alloc]
# Calculate remaining capacity in each account
taxable_left = taxable_cap
roth_left = roth_cap
trad_left = trad_cap
for i in range(n):
taxable_left -= current_alloc[i][0]
roth_left -= current_alloc[i][1]
trad_left -= current_alloc[i][2]
# Process each asset based on the sorted order
for idx in sorted_idx:
delta = deltas[idx]
# Skip if asset is already at its target
if abs(delta) < 1e-6:
continue
# If asset is under-allocated (needs buying)
if delta > 0:
# Add to accounts in order: Roth, Traditional, Taxable
add_roth = min(delta, roth_left)
new_alloc[idx][1] += add_roth
roth_left -= add_roth
delta -= add_roth
add_trad = min(delta, trad_left)
new_alloc[idx][2] += add_trad
trad_left -= add_trad
delta -= add_trad
add_taxable = min(delta, taxable_left)
new_alloc[idx][0] += add_taxable
taxable_left -= add_taxable
# If asset is over-allocated (needs selling)
else:
# Remove from accounts in order: Taxable, Traditional, Roth
remove_taxable = min(-delta, new_alloc[idx][0])
new_alloc[idx][0] -= remove_taxable
delta += remove_taxable
remove_trad = min(-delta, new_alloc[idx][2])
new_alloc[idx][2] -= remove_trad
delta += remove_trad
remove_roth = min(-delta, new_alloc[idx][1])
new_alloc[idx][1] -= remove_roth
delta += remove_roth
# Ensure no allocation is negative (e.g., due to floating point inaccuracies)
for i in range(n):
for j in range(3):
if new_alloc[i][j] < 0:
new_alloc[i][j] = 0.0
return [row[:] for row in new_alloc]
except Exception as e:
return f"Error: {str(e)}"
Live Notebook
Edit this function in a live notebook .