Skip to Content

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 ClassTaxableRothTradTargetTax Score
US Stocks18000700050002500090
Bonds9000300080002000060
Cash400020004000800030

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 ClassTaxableRothTrad
US Stocks13000.07000.05000.0
Bonds9000.03000.08000.0
Cash2000.02000.04000.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 ClassTaxableRothTradTargetTax Score
ETF1800000900010
ETF2200000300010

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 ClassTaxableRothTrad
ETF18000.01000.00.0
ETF22000.00.01000.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.

Live Demo

Last updated on