Rebalancer
Package to rebalance and harvest tax losses in an ETF portfolio
Install / Use
/learn @danguetta/RebalancerREADME
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, andFixed 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 ofIVV,SCHX,VV, orVOO. The code allows these securities to be ranked in order of preference; for example,IVVmight be preferred overSCHXto satisfy theUS Large Capallocation. - 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
IVVoverSCHX).
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
holidayslibrary - 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 secretlog_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 alogsfolder - 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
- If it is equal to
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%), comprisingIVV,SCHX,VV,VOO, andIWB, in that order of preference (except forIWB, 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%), comprisingIJH,VO,SCHM, andIWR, in that order of preference (except forIWR, 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%), comprisingIJR,SCHA,VB,VXF, andIWM, in that order of preference (except forVXFandIWM, 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%), comprisingVEA,IEFA,SCHF, andVEU, in that order of preferenceInternational emerging markets(5%), comprisingVWO,IEMG, andSCHE, in that order of preferenceReal estate(5%), comprisingVNQ,SCHH,USRT, andRWR, in that order of preference (except forRWR, 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%), comprisingSUB, andSHM, in that order of preferenceFixed income mid and long term(7%), comprisingMUB,VTEB,TFI, andITM, in that order of preference (except forITM, 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,41for the first asset class above)name(string) : the name of this asset class (for example1. US Large Capfor 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 beNone. (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
node-connect
345.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
106.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.9kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
