SkillAgentSearch skills...

Rebalancer

Package to rebalance and harvest tax losses in an ETF portfolio

Install / Use

/learn @danguetta/Rebalancer
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

TL;DR: see the sample_output.html file in the repo for an example of what this code can do with a sample portfolio (click on the file name to preview the HTML file directly).

Etrade Portfolio Rebalancer with tax-loss harvesting

This repo contains code required to rebalance and harvest tax losses in a simple portfolio, using the eTrade API.

  • The code first allows the developer to specify a target portfolio, comprising a number of asset classes, each with a target percentage. For example, the target portfolio might comprise three asset classes - US Large Cap, Real Estate, and Fixed income, and target for 60% of the portfolio to comprise the first asset class, 30% the second, and 10% the third.
  • For each asset class, the developer specifies securities that can be bought to own this asset class. For example, to own US Large Cap, the portfolio might buy one or more of IVV, SCHX, VV, or VOO. The code allows these securities to be ranked in order of preference; for example, IVV might be preferred over SCHX to satisfy the US Large Cap allocation.
  • Every time the script is run, it will obtain the current state of the portfolio using the eTrade API, and then automatically carry out buys and sells to meet the following aims
    • End with a portfolio in which the percentage of each asset class owned is as close as possible to the target
    • Sell as many losing positions as possible to harvest tax losses, and replace them with an alternative security in that asset class, all while avoiding wash sales (see this link for an introduction to tax loss harvesting).
    • For each asset class, prioritize higher preference securities over lower ones (in the example from the previous bullet, we would prefer to buy IVV over SCHX).

As a side benefit, the code also provides a convenient interface to access the eTrade API for common opperations, and code to display the current state of a portfolio in a user-friendly fashion.

If this sounds very similar to what RoboAdvisors like Wealthfront or Betterment do, this is no coincidence - I started on these platforms, and then developed this script as a way to transition away from them and do this rebalancing myself.

Note that I am the furthest thing from a financial adviser or investment professional, and this code comes as-is with no guarantees. For all I know, it's completely wrong, so check the code carefully before using it yourself.

Quickstart guide

Begin by importing the rebalancer code.

import rebalancer as u

You may need to install any missing libraries first; in particular, you will need

  • The holidays library
  • Version 1.5.1 or later of pandas
  • Version 3.0.0 or later of jinja2

Connecting to eTrade

If you're not quite ready to connect to your live eTrade portfolio, you can experiment with a sample portfolio; jump straight to the downloading account data section.

To connect to the eTrade API, first get an API key by logging in to your account here. Once you have obtained your consumer key and consumer secret, paste them into a configuration file, following the format of the sample_config.ini file in this repo.

Then, connect to the eTrade API by creating an EtradeConnection object

conn = u.EtradeConnection(config_file, log_file)

Use the following arguments to the constructor:

  • config_file (string) : the path to the configuration file containing the consumer key and consumer secret
  • log_file (string) : every time a request is transmitted to the eTrade API, it will be logged. This string will determine where this logging will happen.
    • If it is equal to None, the logs will be printed to a file named using today's date and time in a logs folder
    • If it is equal to 'screen', the logs will be printed to the Jupyter notebook
    • If it is equal to any other string, the logs will be printed to a file with that name

As soon as you create the object, a browser will be launched that will allow you to log in to the eTrade API; you will need to copy the authorization code from that browser window, and paste it into the Jupyter notebook.

Downloading account data

Once the connection is established, the next step is to download portfolio details by creating an Account object.

If you have not connected to the eTrade API and would like to use the sample portfolio, simply initialize the object with no arguments:

account = u.Account()

Otherwise, use

account = u.Account(account_number, conn, validation_folder)

Use the following argumens to the constructor:

  • account_number (string) : the account number to download (each eTrade login can be associated with multiple accounts; find the account number by logging in to the eTrade front-end, and clicking on "show number" next to the account in question).
  • conn : an EtradeConnection object, obtained in the previous section.
  • validation_folder (string, optional) : during my testing, I found one instance in which the data downloaded through the API did not accurately reflect the portfolio. To guard against this eventuality, the code allows you to download a CSV describing the current portfolio from the eTrade front-end (click on "View full portfolio" on the overview page, and click on the down arrow at the top-right-hand corner of the portfolio page), and to compare this CSV to the API downloads. To do this, simply supply the name of the folder in which you store these front-end downloads; the script will compare the API downloads to the last file in that folder alphabetically by file name (so for example, naming files using the convention YYYY-MM-DD HH-MM.csv will always lead the last file to be the most recent one). If any of the assets or quantities in the CSV do not match the API downloads, an error will be thrown. Also, if any of the market values diverge by more than TOLERANCE (a constant defined at the top of the module), an error will be thrown. This is added in as an extra check. No check will be done if this argument is None

As soon as the object is created, the portfolio will be downloaded from eTrade.

Specifying the target portfolio

The code comes pre-loaded with a default target portfolio, comprising the following allocations:

  • US large cap (41%), comprising IVV, SCHX, VV, VOO, and IWB, in that order of preference (except for IWB, which will be counted as part of this asset class if it is already in the portfolio, but will never be bought)
  • US mid cap (11%), comprising IJH, VO, SCHM, and IWR, in that order of preference (except for IWR, which will be counted as part of this asset class if it is already in the portfolio, but will never be bought)
  • US small cap (5%), comprising IJR, SCHA, VB, VXF, and IWM, in that order of preference (except for VXF and IWM, which will be counted as part of this asset class if it is already in the portfolio, but will never be bought)
  • International developed markets (23%), comprising VEA, IEFA, SCHF, and VEU, in that order of preference
  • International emerging markets (5%), comprising VWO, IEMG, and SCHE, in that order of preference
  • Real estate (5%), comprising VNQ, SCHH, USRT, and RWR, in that order of preference (except for RWR, which will be counted as part of this asset class if it is already in the portfolio, but will never be bought)
  • Fixed income short term (3%), comprising SUB, and SHM, in that order of preference
  • Fixed income mid and long term (7%), comprising MUB, VTEB, TFI, and ITM, in that order of preference (except for ITM, which will be counted as part of this asset class if it is already in the portfolio, but will never be bought)

If you would like to use this default portfolio, skip to the next section.

If you could like to specify your own allocation, begin by creating a target portfolio

target_portfolio = TargetPortfolio()

Then, add each asset class as follows

target_portfolio.add_assetclass(target, name, securities, badness_scores)

This function takes the following arguments:

  • target (integer) : the percentage of the portfolio that should comprise this asset class, expressed as an integer between 0 and 100 (for example, 41 for the first asset class above)
  • name (string) : the name of this asset class (for example 1. US Large Cap for the first asset class above); you might want to include a number before the name to make sure the asset classes are sorted in the right order.
  • securities (list of integers) : a list containing the tickers in this asset class (for example, ['IVV', 'SCHX', 'VV', 'VOO', 'IWB'] for the first asset class above)
  • badness_scores (list of integers) : a list containing the "badness scores" for the securities above; the lowest badness score should be 1, and will indicate the security or securities that are most preferred for this asset class. Larger badness scores, indicating less preferred securities, should be consecutive integers. To indicate that a security in the portfolio should count as part of this asset class but shoudl never be bought, the badness score should be None. (For example, for the first asset class above, this list should be [1, 2, 3, 4, None])

Once you have specified all target asset classes, you should run

target_portfolio.validate()

This will check that the targets sum to 100, and that no securities overlap between asset classes. You will not be able to use the target portfolio before it has been validated, and once th

Related Skills

View on GitHub
GitHub Stars26
CategoryDevelopment
Updated7d ago
Forks5

Languages

HTML

Security Score

75/100

Audited on Mar 26, 2026

No findings