Asset Allocation

TipHow these exercises work

Each topic starts with a real scenario and a bar model, then three levels of exercises. Level 1 (Guided) fills in the blanks of a finished function. Level 2 (Practice) applies it to new numbers. Level 3 (Challenge) asks a new question with little scaffolding. The R and Python exercises run in your browser: click Run Code to try, Submit Answer to check, and use the hint or solution if you get stuck. Excel exercises have a solution you can reveal.

1 Book Topics

  • Stocks, bonds, and cash: stocks grow most but can drop 30% to 50%; bonds grow slower and fall less; cash is the emergency fund and sits outside the investment split
  • How much in stocks when you’re young: everyone agrees on mostly stocks; Bogle (roughly your age in bonds, later 80 to 85% stocks), Sethi (80 to 90%), Collins (100% while building), Felix on the Cederburg research (100% stocks, global), Housel (extra cash for peace of mind); 80 to 90% is a reasonable start
  • US vs. international: 60/40 to 80/20 (US/international) is common; the Cederburg research goes to about one third US
  • Target date funds: one fund that sets the split and follows a glide path toward bonds; check the fee and don’t mix it with other funds
  • The three fund portfolio: total US stocks, total international stocks, total US bonds for about 0.05% (for example 60/30/10)
  • Rebalancing: check once a year and act when any part drifts more than about 5 percentage points; send new money to whatever is below target

2 Good Years and Bad Years

A portfolio’s return is a weighted average of what each part earns.

Formula: Portfolio return = (Stock % × Stock return) + (Bond % × Bond return)

Example (stocks +10% average, −40% bad; bonds +4% average, 0% bad): an 80/20 split in a bad year is (0.80 × −40%) + (0.20 × 0%) = −32%, or −$3,200 on $10,000. In an average year it’s (0.80 × 10%) + (0.20 × 4%) = 8.8%, or +$880.

$10,000 in an average year and a bad year, by stock/bond split

$10,000 in an average year and a bad year, by stock/bond split

$10,000 in an average year and a bad year, by stock/bond split

$10,000 in an average year and a bad year, by stock/bond split

The second example adds more splits and two columns compared with 100/0: the average year gain you give up and the bad year loss you avoid. The third uses the real 2008 numbers from the book (stocks about −37%, bonds about +5%), where bonds actually went up.

Read the two columns together. Going from 100/0 to 60/40 gives up $240 in an average year but cuts the bad year loss from $4,000 to $2,400. Each step toward bonds trades a little growth for a softer fall.

2.1 Level 1: Guided

Fill in the blanks to finish portfolio_return(), then find the return of an 80/20 portfolio in a bad year (stocks −40%, bonds 0%). Give a decimal.

Put the stock share in A2 (0.8), the stock return in B2 (-0.40), and the bond return in C2 (0). Find the portfolio return in D2.

=A2*B2 + (1-A2)*C2

This returns -0.32, a loss of $3,200 on $10,000.

Whatever isn’t in stocks is in bonds, and earns the bond return.

def portfolio_return(stock_pct, stock_return, bond_return):
    return stock_pct * stock_return + (1 - stock_pct) * bond_return

portfolio_return(0.80, -0.40, 0)

Whatever isn’t in stocks is in bonds, and earns the bond return.

portfolio_return <- function(stock_pct, stock_return, bond_return) {
  stock_pct * stock_return + (1 - stock_pct) * bond_return
}
portfolio_return(0.80, -0.40, 0)

2.2 Level 2: Practice

A 60/40 portfolio of $10,000 has an average year (stocks +10%, bonds +4%). How many dollars does it gain?

Change A2 to 0.6, B2 to 0.10, and C2 to 0.04, then turn the return into dollars.

=10000 * (A2*B2 + (1-A2)*C2)

This returns $760, versus $1,000 for all stocks.

Stock share 0.60, stock return 0.10, bond return 0.04.

10000 * portfolio_return(0.60, 0.10, 0.04)

Stock share 0.60, stock return 0.10, bond return 0.04.

10000 * portfolio_return(0.60, 0.10, 0.04)

2.3 Level 3: Challenge

In 2008, stocks fell about 37% and bonds rose about 5%. Of the splits 100/0, 90/10, 80/20, 60/40, 40/60, and 0/100, what is the highest stock share that kept the 2008 loss to 15% or less? Give a decimal.

Put the stock shares in A2:A7, the 2008 return in B, and find the largest share with a return of -15% or better.

In B2, filled down: =A2*-0.37 + (1-A2)*0.05. Then:

=MAXIFS(A2:A7, B2:B7, ">=-0.15")

The answer is 0.4 (a 40/60 portfolio lost 11.8%).

Use max(s for s in splits if portfolio_return(s, -0.37, 0.05) >= -0.15).

splits = [1, 0.9, 0.8, 0.6, 0.4, 0]
max(s for s in splits if portfolio_return(s, -0.37, 0.05) >= -0.15)

portfolio_return() is vectorized: pass all splits, then max(splits[...]).

splits <- c(1, 0.9, 0.8, 0.6, 0.4, 0)
returns <- portfolio_return(splits, -0.37, 0.05)
max(splits[returns >= -0.15])

3 How Much to Move When You Rebalance

When one part grows or shrinks, move just enough to get back to the target split.

Formula: Amount to move = (Target % × Total) − Current amount

Example: $10,000 at 80/20, then stocks drop 30%. Stocks are $5,600, bonds $2,000, total $7,600 (stocks about 74%). (0.80 × $7,600) − $5,600 = $480 moved from bonds into stocks.

An 80/20 portfolio before a 30% stock drop, after it, and after rebalancing

An 80/20 portfolio before a 30% stock drop, after it, and after rebalancing

An 80/20 portfolio before a 30% stock drop, after it, and after rebalancing

An 80/20 portfolio before a 30% stock drop, after it, and after rebalancing

The second example runs several stock moves, up and down, and applies the book’s 5 percentage point rule to decide whether to act at all. A negative amount means sell stocks and buy bonds. The last example rebalances with new money only, so nothing gets sold.

Read a positive answer as “buy that much stock,” and a negative answer as “sell that much stock.” A 10% drop only moves the split about 2 points, so the 5 point rule says wait. With $1,000 of new money after the 30% drop, the full $1,000 goes to stocks and the gap closes from $480 to under $300 without selling anything.

3.1 Level 1: Guided

Fill in the blanks to finish rebalance_amount(). A $10,000 80/20 portfolio has $5,600 in stocks and $2,000 in bonds after a 30% stock drop. How much should move into stocks?

Put the target in A2 (0.8), stocks in B2 (5600), and bonds in C2 (2000). Find the amount to move in D2.

=A2*(B2+C2) - B2

This returns $480: sell $480 of bonds and buy stocks.

Where you should be (target × total) minus where you are.

def rebalance_amount(target_pct, total, current_amount):
    return target_pct * total - current_amount

rebalance_amount(0.80, 5600 + 2000, 5600)

Where you should be (target × total) minus where you are.

rebalance_amount <- function(target_pct, total, current_amount) {
  target_pct * total - current_amount
}
rebalance_amount(0.80, 5600 + 2000, 5600)

3.2 Level 2: Practice

Now stocks rise 30% instead: $10,400 in stocks and $2,000 in bonds. How much should move? (A negative answer means sell stocks.)

Change B2 to 10400.

The same formula returns -$480: sell $480 of stocks and buy bonds, selling high.

The total is 10400 + 2000, and the current stock amount is 10400.

rebalance_amount(0.80, 10400 + 2000, 10400)

The total is 10400 + 2000, and the current stock amount is 10400.

rebalance_amount(0.80, 10400 + 2000, 10400)

3.3 Level 3: Challenge

The book’s rule: only rebalance when stocks drift more than 5 percentage points from the 80% target. Starting from $8,000 stocks and $2,000 bonds, stocks move −30%, −10%, −5%, +10%, or +30%. In how many of those cases should you rebalance?

Put the moves in A2:A6. Find stocks, the stock share, and the drift, then count drifts over 5 points.

Stocks in B2: =8000*(1+A2). Share in C2: =B2/(B2+2000)*100. Drift in D2: =ABS(C2-80). Fill down, then =COUNTIF(D2:D6, ">5") returns 1. Only the 30% drop (73.7% stocks) crosses the band.

stocks = 8000 * (1 + stock_move), share is 100 * stocks / (stocks + 2000), drift is (share - 80).abs().

moves["stocks"] = 8000 * (1 + moves["stock_move"])
moves["share"] = 100 * moves["stocks"] / (moves["stocks"] + 2000)
moves["drift"] = (moves["share"] - 80).abs()
(moves["drift"] > 5).sum()

mutate(stocks = 8000 * (1 + stock_move), share = 100 * stocks / (stocks + 2000), drift = abs(share - 80)), then filter(drift > 5) and nrow().

moves |>
  mutate(
    stocks = 8000 * (1 + stock_move),
    share  = 100 * stocks / (stocks + 2000),
    drift  = abs(share - 80)
  ) |>
  filter(drift > 5) |>
  nrow()