liveBook · Manning

Opportunity cost

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

The value of a benefit forgone when choosing one action prevents or limits an alternative action.

Opportunity cost measures the value forgone by harvesting a loss now rather than retaining the possibility of a more valuable later tax outcome. In tax-loss harvesting, that foregone possibility is treated like an American put option: it may become profitable if the asset price falls, but may expire worthless. section 14.3.2

Estimate the cost by pricing an option with inputs that describe the lot and market assumptions:

  • Use the current asset price, strike price, remaining life, annualized volatility, risk-free rate, and dividend yield. section 14.3.2
  • Price an American put with the option-price package and its binomial-tree method rather than deriving an options model. section 14.3.2
  • Accept a harvesting trade only when its tax benefit exceeds both trading costs and opportunity cost. section 14.3.3 section 14.5

Evaluating a loss lot

For a lot trading at a loss, calculate net tax benefit after trading costs and after opportunity cost. Generate a trade only if both results are positive; then select eligible replacement ETFs while avoiding wash sales. section 14.3.3

Listing 14.12 Calculating TLH benefit after opportunity cost
def evaluate_opp_cost(lot: pd.Series, price: float, current_date: dt.date,
                      tax_params: Dict, sigma: float, risk_free: float,
                      div_yield: float):
    """ Evaluate a loss harvest based on the opportunity cost
    vs the tax benefit """
    
    purchase_date = dt.date.fromisoformat(lot['purchase_date'])
    holding_period = (current_date - purchase_date).days
    if holding_period >= tax_params['lt_cutoff']:   #1
        rate_now = rate_later = tax_params['lt_rate']
    else:
        rate_now = tax_params['st_rate']
        rate_later = tax_params['lt_rate']
    price = float(price)

    o_full = Option(european=False, kind='put', s0=price, k=price,
                    t=30, sigma=sigma, r=risk_free, dv=div_yield)
    p_full = o_full.getPrice('BT')   #2

    days_to_switch = tax_params['lt_cutoff'] - holding_period
    if 0 < days_to_switch <= 30:
        o_pre_switch = Option(european=False, kind='put', s0=price,
                              k=price, t=days_to_switch, sigma=sigma,
                              r=risk_free, dv=div_yield)
        p_til_switch = o_pre_switch.getPrice('BT')   #3
        p_after_switch = p_full - p_til_switch   #4

        opp_cost = rate_now * p_til_switch + rate_later * p_after_switch
    else:
        opp_cost = rate_now * p_full

    benefit = rate_now * (lot['purchase_price'] - price)

    return benefit - opp_cost

The function prices the forgone option value from price, volatility, rates, dividends, holding period, and tax rates, then compares it with the tax benefit of harvesting. section 14.3.2

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

How deep it goes
Models the cost as an American put and implements pricing through option-price, while deferring options-pricing theory.
Examples
Uses VTI, ITOT, SCHB, and SPTM loss lots, replacement ETFs, prices, spreads, and generated harvesting trades.
What it assumes
Introduces option pricing through package parameters while explicitly deferring the underlying options-pricing theory.
What you can do after
Apply it via option-price and a harvest-evaluation function to reject trades whose net benefit is not positive.
How it connects
Connects tax-loss harvesting with American put options, trading costs, wash sales, and short-term and long-term capital gains tax rates.

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.