Skip to content

Instantly share code, notes, and snippets.

@ObjectBoxPC
Last active January 5, 2026 17:20
Show Gist options
  • Select an option

  • Save ObjectBoxPC/f83a072f0de0db61b1be42b7db3e1a96 to your computer and use it in GitHub Desktop.

Select an option

Save ObjectBoxPC/f83a072f0de0db61b1be42b7db3e1a96 to your computer and use it in GitHub Desktop.
Rebalance a J.P. Morgan (Chase) investment portfolio from a CSV export of positions
#!/usr/bin/env python3
import csv
import decimal
import itertools
import sys
def parse_desired_weights(weights_file):
weights = { e["Category"]: decimal.Decimal(e["Desired Weight"]) for e in csv.DictReader(weights_file) }
if sum(weights.values()) != 1:
raise Exception("Desired weights do not sum to 100%")
return weights
def parse_holding_categories(holding_categories_file):
return { e["Ticker"]: e["Category"] for e in csv.DictReader(holding_categories_file) }
def aggregate_positions(positions_file, holding_categories):
category_values = {}
# Read until blank line separating data from footnotes
position_lines = itertools.takewhile(lambda r: r.strip() != "", positions_file)
for entry in csv.DictReader(position_lines):
value = decimal.Decimal(entry["Value"].replace(",", ""))
holding = entry["Ticker"] or entry["CUSIP"]
if holding:
category = holding_categories.get(holding)
if not category:
print("Uncategorized holding {}".format(entry["Ticker"]))
else:
print("Unidentified holding valued at {}".format(value))
if not (holding and category):
category = "Uncategorized"
if category not in category_values:
category_values[category] = decimal.Decimal("0.00")
category_values[category] += value
return category_values
def calculate_rebalancings(category_values, desired_weights):
rebalancings = []
total_value = sum(category_values.values())
# Ensure that categories specifically named in desired weights are listed first
# followed by "extra" categories in the current values
categories = list(desired_weights.keys()) + list(set(category_values.keys()).difference(desired_weights.keys()))
for category in categories:
desired_value = total_value * desired_weights.get(category, 0)
actual_value = category_values.get(category, 0)
difference = desired_value - actual_value
if abs(difference) >= 0.01:
rebalancings.append((category, difference))
return rebalancings
if len(sys.argv) < 4:
print("Usage: {} [desired weights] [holding categories] [portfolio export]".format(sys.argv[0]))
sys.exit(1)
with open(sys.argv[1]) as weights_file:
desired_weights = parse_desired_weights(weights_file)
with open(sys.argv[2]) as holding_categories_file:
holding_categories = parse_holding_categories(holding_categories_file)
with open(sys.argv[3]) as positions_file:
category_values = aggregate_positions(positions_file, holding_categories)
rebalancings = calculate_rebalancings(category_values, desired_weights)
for rebalancing in rebalancings:
print("{:20}: {:+0.2f}".format(*rebalancing))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment