liveBook · Manning

Tax-loss harvesting

As explained in Build a Robo-Advisor with Python (From Scratch)

The optimization of investment transactions to capture tax losses or otherwise improve after-tax returns.

Tax-loss harvesting (TLH) selectively sells investments trading below their purchase price, realizes the loss, and reinvests the proceeds in similar but not identical assets to preserve the intended portfolio exposure. Realized losses can offset realized gains or some ordinary income, reducing current taxes while keeping the portfolio invested. It is therefore a recurring, automatable portfolio-management decision rather than a prediction that a declined asset will remain unattractive. section 14.0 section 1.2.4

The economic case has two parts. First, a loss can defer tax: tax avoided now is paid later when replacement assets are eventually sold, so the time value of money may create value. Second, harvesting can contribute to a lower eventual tax rate when a current short-term loss offsets income or gains and a later gain receives long-term treatment. These benefits create future tax liabilities rather than making tax permanently disappear. section 14.1 section 14.5

A harvesting decision must clear practical constraints and costs:

  • Sell only lots with a realized loss, then choose a similar replacement asset that maintains the desired exposure. section 14.0
  • Respect wash-sale restrictions. A wash sale is not illegal, but it removes the current tax benefit and requires cost-basis adjustments for still-held lots involved in the transaction. section 14.5
  • Compare estimated tax benefit with trading costs and opportunity cost; trade only when the benefit exceeds both. section 14.5
  • Prioritize eligible loss lots by tax benefit when not every loss can be harvested, and avoid buying an ETF that is also planned for sale. section 14.3.3
  • Account for horizon and expected future tax rates. A horizon under one year can eliminate rate-conversion value, while higher future tax rates can undermine the strategy. section 14.1.3

Evaluating an ETF loss lot

Suppose several lots of VTI, ITOT, SCHB, and SPTM are held at purchase prices from 90 to 94, while each trades at 80. A system first excludes lots restricted by wash-sale rules, calculates each lot’s loss and applicable tax rate, ranks eligible lots by tax benefit, and selects allowable replacements from the ETF set. In the illustrated evaluation, eligible VTI, SPTM, and SCHB lots are replaced with ITOT; when every holding belongs to the same ETF, only some loss lots may be harvested because a replacement cannot also be sold as part of the harvest. section 14.3.3

Listing 14.14 Full evaluation of a TLH opportunity
def evaluate_harvests_for_etf_set(lots, current_date, prices, tax_params,
                                  sigmas, risk_free, div_yields,
                                  spreads, etf_set):
    """ Decide whether to harvest eligible lots from holdings
    belonging to one set of exchangeable ETFs """
    all_lots = check_all_assets_for_restrictions(lots, prices,
                                                 current_date)
    lots = all_lots[all_lots['sellable']]
    gains = (prices[lots['ticker']].values - lots['purchase_price'])
    lots['gain'] = lots['quantity'] * gains
    lots = lots[lots['gain'] < 0]
    lots['hp'] = \
        list(map(lambda d: (current_date - dt.date.fromisoformat(d)).days,
                 lots['purchase_date']))   #1
    lots['tax_rate'] = tax_params['st_rate']
    lt_lots = lots['hp'][lots['hp'] >= tax_params['lt_cutoff']].index
    lots.loc[lt_lots, 'tax_rate'] = tax_params['lt_rate']
    lots['tax_benefit'] = -lots['gain'] * lots['tax_rate']
    lots = lots.sort_values(by='tax_benefit', ascending=False)   #2

    tickers_by_benefit = lots.groupby('ticker')['tax_benefit'] \
        .sum().sort_values().index.values
    non_buyable = all_lots[all_lots['blocks_buy']]['ticker'].values
    replacements = [_ for _ in etf_set if _ not in tickers_by_benefit
                    and _ not in non_buyable]   #3
    replacements.extend(tickers_by_benefit)
    \Boost{1.8}
    harvests, buying, selling = {}, [], []
    for i, lot in lots.iterrows():
        ticker = lot['ticker']
        lot_replacements = [_ for _ in replacements if _ != ticker
                            and _ not in selling]
        if ticker in buying or len(lot_replacements) == 0:   #4
            continue
        replacement = lot_replacements[0]
        result = evaluate_harvest(lot, replacement, current_date,
                                  prices, tax_params, sigmas[ticker],
                                  risk_free, div_yields[ticker], spreads)
        if result is not None:
            harvests[i] = result
            buying.append(replacement)   #5
            selling.append(ticker)

    return pd.DataFrame(harvests).T

Use the ETF-set evaluator as the decision layer: it filters wash-sale-restricted lots, identifies losses, applies holding-period tax rates, orders losses by tax benefit, and searches for eligible replacement ETFs. section 14.3.3

TLH is most useful when taxes saved today have greater present value than taxes due later, especially for investors with long horizons and tax rates expected to remain stable or fall before liquidation. Its value is not guaranteed: transaction costs, opportunity costs, wash-sale limits, tax rates, volatility, dividends, and portfolio history all affect whether a proposed trade is worthwhile. Estimated benefits can be optimized through backtesting, much like rebalancing decisions. section 14.1.3 section 14.5

How Build a Robo-Advisor with Python (From Scratch) covers it

How deep it goes
Develops an end-to-end ETF decision process using wash-sale checks, tax-benefit ranking, replacement selection, and cost thresholds.
Examples
Works through exchangeable VTI, ITOT, SCHB, and SPTM lots, including a case where all holdings are VTI.
What it assumes
Assumes familiarity with investment lots, gains and losses, while defining the harvesting process and its tax trade-offs.
What you can do after
Apply it via Python functions that generate eligible ETF harvesting trades after evaluating taxes, restrictions, and costs.
How it connects
Connects tax-loss harvesting with robo-advisors, rebalancing, wash-sale rules, capital gains taxes, trading costs, and backtesting.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Build a Robo-Advisor with Python (From Scratch). The section links point to the parts of the book it draws on.