Chapter 5, Problem 12

Data Download

We download the Fama-French 6 Portfolios Formed on Size and Book-to-Market (2×3) monthly value-weighted returns from Professor Kenneth French’s data library.

# Install/load required packages
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("moments"))   install.packages("moments")
library(tidyverse)
library(moments)

# ── Download ──────────────────────────────────────────────────────────────────
url  <- "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/6_Portfolios_2x3_CSV.zip"
tmp  <- tempfile(fileext = ".zip")
download.file(url, tmp, mode = "wb")

# The ZIP contains one CSV; read it
raw <- read_lines(unz(tmp, unzip(tmp, list = TRUE)$Name[1]))

# ── Parse: locate the value-weighted monthly returns block ───────────────────
# French CSVs have multiple blocks separated by blank lines.
# The FIRST block is "Average Value Weighted Returns -- Monthly"
start <- which(str_detect(raw, "Average Value Weighted Returns -- Monthly"))[1] + 1
# Find the next blank-line-separated section header
ends  <- which(raw == "")
end   <- ends[ends > start][1] - 1

data_lines <- raw[start:end]
data_lines <- data_lines[str_detect(data_lines, "^\\s*\\d")]   # keep rows starting with a year

# Parse into a data frame
ff6 <- read_csv(paste(data_lines, collapse = "\n"),
                col_names = c("date", "SmLo", "SmMe", "SmHi",
                              "BgLo", "BgMe", "BgHi"),
                col_types = cols(.default = col_double())) |>
  mutate(date = as.integer(date)) |>
  filter(date >= 193001, date <= 201812)          # Jan 1930 – Dec 2018

glimpse(ff6)
## Rows: 1,068
## Columns: 7
## $ date <int> 193001, 193002, 193003, 193004, 193005, 193006, 193007, 193008, 1…
## $ SmLo <dbl> 6.0309, 1.7589, 8.6803, -7.0960, -3.6140, -17.9836, 6.5234, -3.78…
## $ SmMe <dbl> 9.5193, 1.0717, 11.3312, -1.2542, -2.6937, -16.4522, 3.6401, -1.6…
## $ SmHi <dbl> 8.4726, 4.5687, 10.6873, -3.4819, -2.9869, -19.0393, 2.5703, -2.3…
## $ BgLo <dbl> 7.3577, 3.4688, 6.7576, -2.3380, 0.7015, -17.6952, 4.7126, 1.0212…
## $ BgMe <dbl> 3.3456, 1.8817, 8.4208, -1.7620, -2.2797, -13.1636, 3.5511, -0.66…
## $ BgHi <dbl> 2.8546, 1.2148, 5.3549, -6.6843, -1.4025, -11.8401, 5.2714, -1.61…
# ── Split in half ─────────────────────────────────────────────────────────────
# Total months
n      <- nrow(ff6)
half   <- floor(n / 2)

first  <- ff6[1:half, ]
second <- ff6[(half + 1):n, ]

cat("First half :", first$date[1],  "–", first$date[half],  "(", half, "months )\n")
## First half : 193001 – 197406 ( 534 months )
cat("Second half:", second$date[1], "–", second$date[n - half], "(", n - half, "months )\n")
## Second half: 197407 – 201812 ( 534 months )
# ── Compute statistics for each half ──────────────────────────────────────────
portfolios <- c("SmLo", "SmMe", "SmHi", "BgLo", "BgMe", "BgHi")

compute_stats <- function(df, label) {
  df |>
    select(all_of(portfolios)) |>
    summarise(across(everything(),
                     list(Mean     = mean,
                          SD       = sd,
                          Skewness = skewness,
                          Kurtosis = kurtosis),
                     .names = "{.col}_{.fn}")) |>
    pivot_longer(everything(),
                 names_to  = c("Portfolio", "Stat"),
                 names_sep = "_") |>
    mutate(Half = label)
}

stats <- bind_rows(
  compute_stats(first,  "First half (1930–1974)"),
  compute_stats(second, "Second half (1975–2018)")
)

# Wide table for display
stats_wide <- stats |>
  pivot_wider(names_from = Stat, values_from = value) |>
  arrange(Portfolio, Half)

knitr::kable(stats_wide, digits = 3,
             caption = "Descriptive Statistics — Monthly Value-Weighted Returns (%)")
Descriptive Statistics — Monthly Value-Weighted Returns (%)
Portfolio Half Mean SD Skewness Kurtosis
BgHi First half (1930–1974) 1.187 8.911 1.769 17.468
BgHi Second half (1975–2018) 1.145 4.887 -0.517 5.805
BgLo First half (1930–1974) 0.765 5.709 0.178 9.894
BgLo Second half (1975–2018) 0.978 4.696 -0.334 4.992
BgMe First half (1930–1974) 0.812 6.734 1.712 20.535
BgMe Second half (1975–2018) 1.058 4.339 -0.473 5.653
SmHi First half (1930–1974) 1.484 10.206 2.288 20.076
SmHi Second half (1975–2018) 1.425 5.499 -0.464 7.305
SmLo First half (1930–1974) 0.971 8.225 1.180 12.072
SmLo Second half (1975–2018) 0.996 6.688 -0.409 5.159
SmMe First half (1930–1974) 1.169 8.423 1.580 15.740
SmMe Second half (1975–2018) 1.355 5.282 -0.533 6.425

Discussion

cat("
**Do the two halves suggest the same return distribution?**

The table above compares the mean, standard deviation, skewness, and excess
kurtosis across the six size/value portfolios for the two sub-periods.

Key observations:

1. **Means differ noticeably across sub-periods.** Small-cap and value portfolios
   often show higher average returns in one half than the other, which is
   inconsistent with a single stable distribution.

2. **Standard deviations are higher in the first half**, largely due to the
   Great Depression and World War II era volatility.

3. **Skewness and kurtosis vary across halves**, indicating the shape of the
   return distribution is not constant over time.

**Conclusion:** The split-halves statistics suggest the six portfolios do *not*
come from the same distribution over the entire 1930–2018 period. The first half
is characterised by higher volatility and fatter tails, while the second half
shows more moderate dispersion. This is consistent with structural breaks
(regulatory changes, technological shifts, globalisation) that altered return
dynamics over time.
")

Do the two halves suggest the same return distribution?

The table above compares the mean, standard deviation, skewness, and excess kurtosis across the six size/value portfolios for the two sub-periods.

Key observations:

  1. Means differ noticeably across sub-periods. Small-cap and value portfolios often show higher average returns in one half than the other, which is inconsistent with a single stable distribution.

  2. Standard deviations are higher in the first half, largely due to the Great Depression and World War II era volatility.

  3. Skewness and kurtosis vary across halves, indicating the shape of the return distribution is not constant over time.

Conclusion: The split-halves statistics suggest the six portfolios do not come from the same distribution over the entire 1930–2018 period. The first half is characterised by higher volatility and fatter tails, while the second half shows more moderate dispersion. This is consistent with structural breaks (regulatory changes, technological shifts, globalisation) that altered return dynamics over time.


CFA Problem 1

Given Information

Action Probability Expected Return
Invest in equities 0.6 $50,000
Invest in equities 0.4 −$30,000
Invest in risk-free T-bill 1.0 $5,000

Solution

# ── Inputs ────────────────────────────────────────────────────────────────────
p1       <- 0.6;   r1 <- 50000    # equity outcome 1
p2       <- 0.4;   r2 <- -30000   # equity outcome 2
rf       <- 5000                   # risk-free return (certain)

# ── Expected return of equities ───────────────────────────────────────────────
E_equity <- p1 * r1 + p2 * r2
cat("Expected return (equities) = $", format(E_equity, big.mark = ","), "\n")
## Expected return (equities) = $ 18,000
# ── Risk premium ──────────────────────────────────────────────────────────────
risk_premium <- E_equity - rf
cat("Risk premium               = $", format(risk_premium, big.mark = ","), "\n")
## Risk premium               = $ 13,000

Answer

\[ E[R_{\text{equity}}] = 0.6 \times \$50{,}000 + 0.4 \times (-\$30{,}000) = \$18{,}000 \]

\[ \text{Risk Premium} = E[R_{\text{equity}}] - R_f = \$18{,}000 - \$5{,}000 = \mathbf{\$13{,}000} \]

The expected risk premium of investing in equities versus risk-free T-bills is $13,000.


Trading Record Update (HW 01 → HW 02)

Portfolio Entry — March 3, 2026

Asset Type Entry Price Shares Invested
QQQ ETF $440.00 45 $19,800
SPY ETF $510.00 39 $19,890
AAPL Stock $263.75 75 $19,781
NVDA Stock $180.90 110 $19,899
JNJ Stock $160.00 62 $9,920
KO Stock $60.00 166 $9,960
Total $98,711

Cash remaining: $1,289

Portfolio Update — March 8, 2026

portfolio <- tibble(
  Asset        = c("QQQ", "SPY", "AAPL", "NVDA", "JNJ", "KO"),
  Type         = c("ETF","ETF","Stock","Stock","Stock","Stock"),
  Entry_Price  = c(440.00, 510.00, 263.75, 180.90, 160.00, 60.00),
  Current_Price= c(420.05, 488.65, 257.46, 177.82, 161.00, 63.04),
  Shares       = c(45, 39, 75, 110, 62, 166)
) |>
  mutate(
    Cost_Basis      = Entry_Price * Shares,
    Market_Value    = Current_Price * Shares,
    Unrealized_PL   = Market_Value - Cost_Basis,
    Unrealized_PL_pct = round(Unrealized_PL / Cost_Basis * 100, 2)
  )

knitr::kable(
  portfolio |> select(Asset, Type, Entry_Price, Current_Price,
                       Shares, Unrealized_PL, Unrealized_PL_pct),
  digits  = 2,
  col.names = c("Asset","Type","Entry ($)","Current ($)",
                "Shares","Unreal. P/L ($)","Unreal. P/L (%)"),
  caption = "Portfolio Status — March 8, 2026"
)
Portfolio Status — March 8, 2026
Asset Type Entry (\()| Current (\)) Shares Unreal. P/L ($) Unreal. P/L (%)
QQQ ETF 440.00 420.05 45 -897.75 -4.53
SPY ETF 510.00 488.65 39 -832.65 -4.19
AAPL Stock 263.75 257.46 75 -471.75 -2.38
NVDA Stock 180.90 177.82 110 -338.80 -1.70
JNJ Stock 160.00 161.00 62 62.00 0.62
KO Stock 60.00 63.04 166 504.64 5.07
cat("\nTotal cost basis  : $", format(sum(portfolio$Cost_Basis),   big.mark=",", nsmall=2), "\n")
## 
## Total cost basis  : $ 99,250.25
cat("Total market value: $", format(sum(portfolio$Market_Value),  big.mark=",", nsmall=2), "\n")
## Total market value: $ 97,275.94
cat("Total P/L         : $", format(sum(portfolio$Unrealized_PL), big.mark=",", nsmall=2), "\n")
## Total P/L         : $ -1,974.31

Decision Summary

Asset Status Decision Rationale
QQQ Underperformer Hold Still above −8% stop-loss; monitoring
SPY Underperformer Hold Still above −8% stop-loss; monitoring
AAPL −2.38% Hold Well above −10% stop-loss threshold
NVDA −1.72% Hold Well above −10% stop-loss threshold
JNJ +0.63% Long-term hold Defensive position; stable dividend
KO +5.07% Long-term hold Defensive position; brand strength

Note on current prices: QQQ and SPY have experienced pullbacks from early March highs due to macro headwinds (tariff uncertainty, Fed rate expectations). NVDA and AAPL remain under mild pressure. JNJ and KO are performing in line with their defensive role. No stop-losses have been triggered; all positions are maintained.