CAPM & Multi-Factor Models Analysis

Tesla, Toyota, GM - Part 2 MPT Assignment

Author

Liam

Published

December 3, 2025

1 Introduction

This document provides a step-by-step analysis of Tesla (TSLA), Toyota (TM), and General Motors (GM) using:

  1. CAPM (Capital Asset Pricing Model)
  2. Fama-French 3-Factor Model (FF3)
  3. Carhart 4-Factor Model (FF3 + Momentum)

We will answer Questions 1-9 from Part 2 of the MPT assignment.


2 Setup and Data Preparation

2.1 Load Required Packages

# Install packages if needed (uncomment if necessary)
# install.packages(c("quantmod", "dplyr", "tidyr", "readr", "lubridate", "knitr", "broom"))

library(quantmod)   # For downloading stock data
library(dplyr)      # For data manipulation
library(tidyr)      # For reshaping data
library(readr)      # For reading CSV files
library(lubridate)  # For date handling
library(knitr)      # For nice tables
library(broom)      # For tidy regression output

2.2 Download Stock Price Data

We download adjusted closing prices for Tesla, Toyota, and GM from Yahoo Finance.

# Define tickers and date range
tickers <- c("TSLA", "TM", "GM")
start_date <- "2010-12-01"
end_date <- Sys.Date()

# Download data for each stock
# getSymbols returns data to the global environment by default
getSymbols(tickers, src = "yahoo", from = start_date, to = end_date, auto.assign = TRUE)
[1] "TSLA" "TM"   "GM"  
# Extract adjusted closing prices into a single xts object
prices <- merge(Ad(TSLA), Ad(TM), Ad(GM))
colnames(prices) <- c("TSLA", "TM", "GM")

# Preview the data
head(prices)
               TSLA       TM       GM
2010-12-01 2.290000 54.31368 25.98766
2010-12-02 2.156667 53.93148 25.91294
2010-12-03 2.099333 53.95196 25.81580
2010-12-06 2.020667 54.01338 25.76350
2010-12-07 2.104000 53.69262 25.91294
2010-12-08 2.158000 53.48103 25.74109
tail(prices)
             TSLA     TM    GM
2025-11-24 417.78 199.06 71.00
2025-11-25 419.40 200.26 72.78
2025-11-26 426.58 202.44 72.81
2025-11-28 430.17 201.87 73.52
2025-12-01 430.14 199.22 72.95
2025-12-02 429.24 196.99 73.66

2.3 Calculate Monthly Returns

We convert daily prices to monthly prices, then calculate simple returns.

# Convert daily to monthly (last observation of each month)
monthly_prices <- to.monthly(prices, indexAt = "lastof", OHLC = FALSE)

# Calculate simple returns: (P_t - P_{t-1}) / P_{t-1}
# We use Return.calculate from PerformanceAnalytics or do it manually
monthly_returns <- diff(monthly_prices) / lag(monthly_prices, 1)

# Remove the first NA row
monthly_returns <- monthly_returns[-1, ]

# Convert to data frame for easier manipulation
returns_df <- data.frame(
  date = index(monthly_returns),
  coredata(monthly_returns)
)

# Preview
head(returns_df)
        date         TSLA           TM           GM
1 2011-01-31 -0.095005275  0.045148167 -0.010038051
2 2011-02-28 -0.008713727  0.135313100 -0.081117981
3 2011-03-31  0.161573664 -0.131841473 -0.074560044
4 2011-04-30 -0.005405400 -0.007102854  0.034160439
5 2011-05-31  0.092028728  0.045306383 -0.008725489
6 2011-06-30 -0.033510059 -0.010445451 -0.045583025

2.4 Download Fama-French Factors

We download the Fama-French 3 factors (Mkt-RF, SMB, HML) and the Momentum factor from Kenneth French’s website.

# URL for Fama-French 3 Factors (Monthly)
ff3_url <- "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip"

# URL for Momentum Factor (Monthly)
mom_url <- "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Momentum_Factor_CSV.zip"

# Download and extract FF3 factors
temp_ff3 <- tempfile()
download.file(ff3_url, temp_ff3, mode = "wb")
unzip(temp_ff3, exdir = tempdir())

# Read the CSV (skip header rows, data starts after some text)
ff3_raw <- read.csv(file.path(tempdir(), "F-F_Research_Data_Factors.CSV"), skip = 3)

# The file has annual data at the bottom after a blank row - we need to find where monthly ends
# Monthly data has 6-digit dates (YYYYMM), annual has 4-digit (YYYY)
ff3_raw <- ff3_raw %>%
  rename(date = X) %>%
  filter(nchar(trimws(date)) == 6)  # Keep only monthly data (YYYYMM format)

# Convert to numeric and proper date format
ff3_clean <- ff3_raw %>%
  mutate(
    date = as.Date(paste0(date, "01"), format = "%Y%m%d"),
    date = ceiling_date(date, "month") - days(1),  # Last day of month
    Mkt_RF = as.numeric(Mkt.RF) / 100,  # Convert from percentage
    SMB = as.numeric(SMB) / 100,
    HML = as.numeric(HML) / 100,
    RF = as.numeric(RF) / 100
  ) %>%
  select(date, Mkt_RF, SMB, HML, RF)

# Download and extract Momentum factor
temp_mom <- tempfile()
download.file(mom_url, temp_mom, mode = "wb")
unzip(temp_mom, exdir = tempdir())

mom_raw <- read.csv(file.path(tempdir(), "F-F_Momentum_Factor.CSV"), skip = 13)

mom_clean <- mom_raw %>%
  rename(date = X, MOM = Mom) %>%
  filter(nchar(trimws(date)) == 6) %>%
  mutate(
    date = as.Date(paste0(date, "01"), format = "%Y%m%d"),
    date = ceiling_date(date, "month") - days(1),
    MOM = as.numeric(MOM) / 100
  ) %>%
  select(date, MOM)

# Preview
head(ff3_clean)
        date  Mkt_RF     SMB     HML     RF
1 1926-07-31  0.0289 -0.0255 -0.0239 0.0022
2 1926-08-31  0.0264 -0.0114  0.0381 0.0025
3 1926-09-30  0.0038 -0.0136  0.0005 0.0023
4 1926-10-31 -0.0327 -0.0014  0.0082 0.0032
5 1926-11-30  0.0254 -0.0011 -0.0061 0.0031
6 1926-12-31  0.0262 -0.0007  0.0006 0.0028
head(mom_clean)
        date     MOM
1 1927-01-31  0.0057
2 1927-02-28 -0.0150
3 1927-03-31  0.0352
4 1927-04-30  0.0436
5 1927-05-31  0.0278
6 1927-06-30  0.0055

2.5 Merge Stock Returns with Factors

# Merge all data together
analysis_df <- returns_df %>%
  inner_join(ff3_clean, by = "date") %>%
  inner_join(mom_clean, by = "date")

# Calculate excess returns (stock return minus risk-free rate)
analysis_df <- analysis_df %>%
  mutate(
    TSLA_excess = TSLA - RF,
    TM_excess = TM - RF,
    GM_excess = GM - RF
  )

# Preview the final dataset
head(analysis_df)
        date         TSLA           TM           GM  Mkt_RF     SMB     HML
1 2011-01-31 -0.095005275  0.045148167 -0.010038051  0.0198 -0.0229  0.0061
2 2011-02-28 -0.008713727  0.135313100 -0.081117981  0.0348  0.0150  0.0118
3 2011-03-31  0.161573664 -0.131841473 -0.074560044  0.0045  0.0249 -0.0184
4 2011-04-30 -0.005405400 -0.007102854  0.034160439  0.0290 -0.0042 -0.0244
5 2011-05-31  0.092028728  0.045306383 -0.008725489 -0.0127 -0.0073 -0.0185
6 2011-06-30 -0.033510059 -0.010445451 -0.045583025 -0.0174 -0.0027 -0.0020
     RF     MOM  TSLA_excess    TM_excess    GM_excess
1 1e-04 -0.0030 -0.095105275  0.045048167 -0.010138051
2 1e-04  0.0188 -0.008813727  0.135213100 -0.081217981
3 1e-04  0.0345  0.161473664 -0.131941473 -0.074660044
4 0e+00  0.0005 -0.005405400 -0.007102854  0.034160439
5 0e+00 -0.0060  0.092028728  0.045306383 -0.008725489
6 0e+00  0.0182 -0.033510059 -0.010445451 -0.045583025
cat("\nNumber of observations:", nrow(analysis_df), "\n")

Number of observations: 178 
cat("Date range:", as.character(min(analysis_df$date)), "to", as.character(max(analysis_df$date)), "\n")
Date range: 2011-01-31 to 2025-10-31 

3 Question 1 & 2: CAPM and Alpha Calculation

3.1 Conceptual Background

CAPM Formula: \[E[R_i] = R_f + \beta_i (E[R_m] - R_f)\]

Alpha Formula: \[\alpha = R_{actual} - [R_f + \beta(R_m - R_f)]\]

Key Insight: Comparing raw returns is flawed because higher returns may simply reflect higher beta (market risk), not skill. We must adjust for systematic risk.

3.2 Run CAPM Regressions

The CAPM regression is: \[R_i - R_f = \alpha + \beta (R_m - R_f) + \epsilon\]

# CAPM for Tesla
capm_tsla <- lm(TSLA_excess ~ Mkt_RF, data = analysis_df)

# CAPM for Toyota
capm_tm <- lm(TM_excess ~ Mkt_RF, data = analysis_df)

# CAPM for GM
capm_gm <- lm(GM_excess ~ Mkt_RF, data = analysis_df)

3.3 CAPM Results

3.3.1 Tesla CAPM

summary(capm_tsla)

Call:
lm(formula = TSLA_excess ~ Mkt_RF, data = analysis_df)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.33461 -0.09652 -0.02246  0.08987  0.73429 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.02480    0.01245   1.992   0.0479 *  
Mkt_RF       1.84331    0.28578   6.450 1.04e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.161 on 176 degrees of freedom
Multiple R-squared:  0.1912,    Adjusted R-squared:  0.1866 
F-statistic:  41.6 on 1 and 176 DF,  p-value: 1.044e-09

Interpretation:

  • Beta (β): The coefficient on Mkt_RF. A beta > 1 means the stock amplifies market movements.
  • Alpha (α): The intercept. Positive alpha suggests outperformance after adjusting for market risk.
  • R²: Proportion of return variation explained by the market factor.

3.3.2 Toyota CAPM

summary(capm_tm)

Call:
lm(formula = TM_excess ~ Mkt_RF, data = analysis_df)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.136287 -0.030614 -0.002533  0.029780  0.168475 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 0.001664   0.004069   0.409    0.683    
Mkt_RF      0.596021   0.093420   6.380 1.52e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.05262 on 176 degrees of freedom
Multiple R-squared:  0.1878,    Adjusted R-squared:  0.1832 
F-statistic:  40.7 on 1 and 176 DF,  p-value: 1.515e-09

3.3.3 GM CAPM

summary(capm_gm)

Call:
lm(formula = GM_excess ~ Mkt_RF, data = analysis_df)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.172897 -0.047033 -0.002003  0.036413  0.225198 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -0.007094   0.005433  -1.306    0.193    
Mkt_RF       1.436581   0.124715  11.519   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.07025 on 176 degrees of freedom
Multiple R-squared:  0.4298,    Adjusted R-squared:  0.4266 
F-statistic: 132.7 on 1 and 176 DF,  p-value: < 2.2e-16

3.4 CAPM Summary Table

# Extract key statistics
capm_summary <- data.frame(
  Stock = c("Tesla", "Toyota", "GM"),
  Alpha = c(coef(capm_tsla)[1], coef(capm_tm)[1], coef(capm_gm)[1]),
  Alpha_pvalue = c(
    summary(capm_tsla)$coefficients[1, 4],
    summary(capm_tm)$coefficients[1, 4],
    summary(capm_gm)$coefficients[1, 4]
  ),
  Beta = c(coef(capm_tsla)[2], coef(capm_tm)[2], coef(capm_gm)[2]),
  Beta_pvalue = c(
    summary(capm_tsla)$coefficients[2, 4],
    summary(capm_tm)$coefficients[2, 4],
    summary(capm_gm)$coefficients[2, 4]
  ),
  R_squared = c(
    summary(capm_tsla)$r.squared,
    summary(capm_tm)$r.squared,
    summary(capm_gm)$r.squared
  )
)

# Format for display
capm_summary %>%
  mutate(
    Alpha = paste0(round(Alpha * 100, 2), "%"),
    Alpha_pvalue = round(Alpha_pvalue, 4),
    Beta = round(Beta, 2),
    Beta_pvalue = formatC(Beta_pvalue, format = "e", digits = 2),
    R_squared = paste0(round(R_squared * 100, 1), "%")
  ) %>%
  kable(col.names = c("Stock", "Alpha (monthly)", "Alpha p-value", "Beta", "Beta p-value", "R²"))
Stock Alpha (monthly) Alpha p-value Beta Beta p-value
Tesla 2.48% 0.0479 1.84 1.04e-09 19.1%
Toyota 0.17% 0.6832 0.60 1.52e-09 18.8%
GM -0.71% 0.1933 1.44 3.07e-23 43%

4 Question 3: Skill vs Luck

4.1 Conceptual Background

Key Question: Is positive alpha evidence of skill or just luck?

The Problem:

  • Idiosyncratic (firm-specific) risk creates noise in alpha estimates
  • One year of data is insufficient to distinguish skill from luck
  • We need statistical significance to have confidence in alpha

Significance Criteria:

  • t-statistic > 2
  • p-value < 0.05
# Check t-statistics for alpha
cat("Tesla Alpha t-statistic:", round(summary(capm_tsla)$coefficients[1, 3], 2), "\n")
Tesla Alpha t-statistic: 1.99 
cat("Toyota Alpha t-statistic:", round(summary(capm_tm)$coefficients[1, 3], 2), "\n")
Toyota Alpha t-statistic: 0.41 
cat("GM Alpha t-statistic:", round(summary(capm_gm)$coefficients[1, 3], 2), "\n")
GM Alpha t-statistic: -1.31 

Interpretation: Even with multiple years of data, none of our stocks show clearly significant alpha at the 5% level in the CAPM framework. This illustrates how difficult it is to prove “skill” statistically.


5 Question 4: Cyclical vs Defensive Classification

5.1 Classification Rule

  • β > 1: Cyclical stock (amplifies market movements)
  • β < 1: Defensive stock (dampens market movements)

5.2 Results from CAPM

cat("CAPM Betas:\n")
CAPM Betas:
cat("Tesla: β =", round(coef(capm_tsla)[2], 2), "→", ifelse(coef(capm_tsla)[2] > 1, "Cyclical", "Defensive"), "\n")
Tesla: β = 1.84 → Cyclical 
cat("Toyota: β =", round(coef(capm_tm)[2], 2), "→", ifelse(coef(capm_tm)[2] > 1, "Cyclical", "Defensive"), "\n")
Toyota: β = 0.6 → Defensive 
cat("GM: β =", round(coef(capm_gm)[2], 2), "→", ifelse(coef(capm_gm)[2] > 1, "Cyclical", "Defensive"), "\n")
GM: β = 1.44 → Cyclical 

We will compare this with FF3 betas in the next section.


6 Questions 5-7: Fama-French 3-Factor Model

6.1 Conceptual Background

The FF3 model adds two factors to CAPM:

\[R_i - R_f = \alpha + \beta (R_m - R_f) + s \cdot SMB + h \cdot HML + \epsilon\]

Where:

  • SMB (Small Minus Big): Size factor. Positive loading = behaves like small-cap.
  • HML (High Minus Low): Value factor. Positive loading = behaves like value stock; negative = growth stock.

6.2 Run FF3 Regressions

# FF3 for Tesla
ff3_tsla <- lm(TSLA_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

# FF3 for Toyota
ff3_tm <- lm(TM_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

# FF3 for GM
ff3_gm <- lm(GM_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

6.3 FF3 Results

6.3.1 Tesla FF3

summary(ff3_tsla)

Call:
lm(formula = TSLA_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.30462 -0.10715 -0.01737  0.07397  0.74808 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.02759    0.01215   2.271  0.02438 *  
Mkt_RF       1.63721    0.29199   5.607 7.95e-08 ***
SMB          1.16879    0.48206   2.425  0.01635 *  
HML         -1.14880    0.35507  -3.235  0.00145 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.1552 on 174 degrees of freedom
Multiple R-squared:  0.2563,    Adjusted R-squared:  0.2435 
F-statistic: 19.99 on 3 and 174 DF,  p-value: 3.509e-11

6.3.2 Toyota FF3

summary(ff3_tm)

Call:
lm(formula = TM_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.130513 -0.032114 -0.003858  0.024294  0.170828 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.0008251  0.0040965   0.201   0.8406    
Mkt_RF       0.6473355  0.0984470   6.575 5.45e-10 ***
SMB         -0.2729047  0.1625309  -1.679   0.0949 .  
HML          0.1391050  0.1197170   1.162   0.2468    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.05234 on 174 degrees of freedom
Multiple R-squared:  0.2056,    Adjusted R-squared:  0.1919 
F-statistic: 15.01 on 3 and 174 DF,  p-value: 9.825e-09

6.3.3 GM FF3

summary(ff3_gm)

Call:
lm(formula = GM_excess ~ Mkt_RF + SMB + HML, data = analysis_df)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.168934 -0.041226 -0.003239  0.038771  0.196147 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -0.004056   0.005177  -0.783  0.43446    
Mkt_RF       1.306740   0.124414  10.503  < 2e-16 ***
SMB          0.575196   0.205401   2.800  0.00568 ** 
HML          0.583813   0.151294   3.859  0.00016 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.06615 on 174 degrees of freedom
Multiple R-squared:  0.5002,    Adjusted R-squared:  0.4916 
F-statistic: 58.06 on 3 and 174 DF,  p-value: < 2.2e-16

6.4 FF3 Summary Table

ff3_summary <- data.frame(
  Stock = c("Tesla", "Toyota", "GM"),
  Alpha = c(coef(ff3_tsla)[1], coef(ff3_tm)[1], coef(ff3_gm)[1]),
  Alpha_p = c(
    summary(ff3_tsla)$coefficients[1, 4],
    summary(ff3_tm)$coefficients[1, 4],
    summary(ff3_gm)$coefficients[1, 4]
  ),
  Beta = c(coef(ff3_tsla)[2], coef(ff3_tm)[2], coef(ff3_gm)[2]),
  SMB = c(coef(ff3_tsla)[3], coef(ff3_tm)[3], coef(ff3_gm)[3]),
  SMB_p = c(
    summary(ff3_tsla)$coefficients[3, 4],
    summary(ff3_tm)$coefficients[3, 4],
    summary(ff3_gm)$coefficients[3, 4]
  ),
  HML = c(coef(ff3_tsla)[4], coef(ff3_tm)[4], coef(ff3_gm)[4]),
  HML_p = c(
    summary(ff3_tsla)$coefficients[4, 4],
    summary(ff3_tm)$coefficients[4, 4],
    summary(ff3_gm)$coefficients[4, 4]
  ),
  R2 = c(
    summary(ff3_tsla)$r.squared,
    summary(ff3_tm)$r.squared,
    summary(ff3_gm)$r.squared
  )
)

ff3_summary %>%
  mutate(
    Alpha = paste0(round(Alpha * 100, 2), "%"),
    Alpha_p = round(Alpha_p, 3),
    Beta = round(Beta, 2),
    SMB = round(SMB, 2),
    SMB_p = round(SMB_p, 4),
    HML = round(HML, 2),
    HML_p = round(HML_p, 4),
    R2 = paste0(round(R2 * 100, 1), "%")
  ) %>%
  kable(col.names = c("Stock", "Alpha", "α p-val", "Beta", "SMB", "SMB p-val", "HML", "HML p-val", "R²"))
Stock Alpha α p-val Beta SMB SMB p-val HML HML p-val
Tesla 2.76% 0.024 1.64 1.17 0.0163 -1.15 0.0015 25.6%
Toyota 0.08% 0.841 0.65 -0.27 0.0949 0.14 0.2468 20.6%
GM -0.41% 0.434 1.31 0.58 0.0057 0.58 0.0002 50%

6.5 Question 4 Revisited: FF3 Betas

cat("FF3 Market Betas:\n")
FF3 Market Betas:
cat("Tesla: β =", round(coef(ff3_tsla)[2], 2), "→", ifelse(coef(ff3_tsla)[2] > 1, "Cyclical", "Defensive"), "\n")
Tesla: β = 1.64 → Cyclical 
cat("Toyota: β =", round(coef(ff3_tm)[2], 2), "→", ifelse(coef(ff3_tm)[2] > 1, "Cyclical", "Defensive"), "\n")
Toyota: β = 0.65 → Defensive 
cat("GM: β =", round(coef(ff3_gm)[2], 2), "→", ifelse(coef(ff3_gm)[2] > 1, "Cyclical", "Defensive"), "\n")
GM: β = 1.31 → Cyclical 
cat("\nClassification unchanged from CAPM.\n")

Classification unchanged from CAPM.

6.6 Question 5: Size Factor Interpretation

Decision Rule (5% significance):

  • s > 0 and p < 0.05 → Small-cap behavior
  • s < 0 and p < 0.05 → Large-cap behavior
  • p > 0.05 → Cannot classify
cat("Size Factor (SMB) Results:\n\n")
Size Factor (SMB) Results:
# Tesla
cat("Tesla: s =", round(coef(ff3_tsla)[3], 2), ", p =", round(summary(ff3_tsla)$coefficients[3, 4], 4))
Tesla: s = 1.17 , p = 0.0163
if (summary(ff3_tsla)$coefficients[3, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_tsla)[3] > 0, "Small-cap behavior", "Large-cap behavior"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 →  Small-cap behavior 
# Toyota
cat("Toyota: s =", round(coef(ff3_tm)[3], 2), ", p =", round(summary(ff3_tm)$coefficients[3, 4], 4))
Toyota: s = -0.27 , p = 0.0949
if (summary(ff3_tm)$coefficients[3, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_tm)[3] > 0, "Small-cap behavior", "Large-cap behavior"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 → Not significant (cannot classify)
# GM
cat("GM: s =", round(coef(ff3_gm)[3], 2), ", p =", round(summary(ff3_gm)$coefficients[3, 4], 4))
GM: s = 0.58 , p = 0.0057
if (summary(ff3_gm)$coefficients[3, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_gm)[3] > 0, "Small-cap behavior", "Large-cap behavior"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 →  Small-cap behavior 

Key Finding: GM behaves like a small-cap stock despite being a large company. This may reflect its financial distress history and higher volatility.

6.7 Question 6: Value/Growth Interpretation

Decision Rule (5% significance):

  • h > 0 and p < 0.05 → Value stock
  • h < 0 and p < 0.05 → Growth stock
  • p > 0.05 → Cannot classify
cat("Value/Growth Factor (HML) Results:\n\n")
Value/Growth Factor (HML) Results:
# Tesla
cat("Tesla: h =", round(coef(ff3_tsla)[4], 2), ", p =", round(summary(ff3_tsla)$coefficients[4, 4], 4))
Tesla: h = -1.15 , p = 0.0015
if (summary(ff3_tsla)$coefficients[4, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_tsla)[4] > 0, "Value stock", "Growth stock"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 →  Growth stock 
# Toyota
cat("Toyota: h =", round(coef(ff3_tm)[4], 2), ", p =", round(summary(ff3_tm)$coefficients[4, 4], 4))
Toyota: h = 0.14 , p = 0.2468
if (summary(ff3_tm)$coefficients[4, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_tm)[4] > 0, "Value stock", "Growth stock"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 → Not significant (cannot classify)
# GM
cat("GM: h =", round(coef(ff3_gm)[4], 2), ", p =", round(summary(ff3_gm)$coefficients[4, 4], 4))
GM: h = 0.58 , p = 2e-04
if (summary(ff3_gm)$coefficients[4, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff3_gm)[4] > 0, "Value stock", "Growth stock"), "\n")
} else {
  cat(" → Not significant (cannot classify)\n")
}
 →  Value stock 

Key Finding: Tesla is a growth stock (negative HML loading, significant). GM is a value stock (positive HML loading, significant). This aligns with their business profiles.

6.8 Question 7: Alpha Significance in FF3

cat("FF3 Alpha Results:\n\n")
FF3 Alpha Results:
cat("Tesla: α =", round(coef(ff3_tsla)[1] * 100, 2), "% monthly, p =", round(summary(ff3_tsla)$coefficients[1, 4], 3))
Tesla: α = 2.76 % monthly, p = 0.024
cat(ifelse(summary(ff3_tsla)$coefficients[1, 4] < 0.05, " → Significant\n", " → Not significant\n"))
 → Significant
cat("Toyota: α =", round(coef(ff3_tm)[1] * 100, 2), "% monthly, p =", round(summary(ff3_tm)$coefficients[1, 4], 3))
Toyota: α = 0.08 % monthly, p = 0.841
cat(ifelse(summary(ff3_tm)$coefficients[1, 4] < 0.05, " → Significant\n", " → Not significant\n"))
 → Not significant
cat("GM: α =", round(coef(ff3_gm)[1] * 100, 2), "% monthly, p =", round(summary(ff3_gm)$coefficients[1, 4], 3))
GM: α = -0.41 % monthly, p = 0.434
cat(ifelse(summary(ff3_gm)$coefficients[1, 4] < 0.05, " → Significant\n", " → Not significant\n"))
 → Not significant

7 Question 8: R² Analysis

7.1 Comparison of Model Fit

r2_comparison <- data.frame(
  Stock = c("Tesla", "Toyota", "GM"),
  CAPM_R2 = c(
    summary(capm_tsla)$r.squared,
    summary(capm_tm)$r.squared,
    summary(capm_gm)$r.squared
  ),
  FF3_R2 = c(
    summary(ff3_tsla)$r.squared,
    summary(ff3_tm)$r.squared,
    summary(ff3_gm)$r.squared
  )
)

r2_comparison %>%
  mutate(
    CAPM_R2 = paste0(round(CAPM_R2 * 100, 1), "%"),
    FF3_R2 = paste0(round(FF3_R2 * 100, 1), "%")
  ) %>%
  kable(col.names = c("Stock", "CAPM R²", "FF3 R²"))
Stock CAPM R² FF3 R²
Tesla 19.1% 25.6%
Toyota 18.8% 20.6%
GM 43% 50%

7.2 Interpretation

cat("R² Interpretation:\n\n")
R² Interpretation:
cat("• Higher R² = more return variation explained by systematic factors\n")
• Higher R² = more return variation explained by systematic factors
cat("• Lower R² = more firm-specific (idiosyncratic) risk\n\n")
• Lower R² = more firm-specific (idiosyncratic) risk
cat("GM (highest R²): Returns well-explained by systematic factors.\n")
GM (highest R²): Returns well-explained by systematic factors.
cat("  - Mature automaker with predictable market sensitivity\n\n")
  - Mature automaker with predictable market sensitivity
cat("Tesla (lowest R²): Returns dominated by firm-specific factors.\n")
Tesla (lowest R²): Returns dominated by firm-specific factors.
cat("  - Innovation news, Elon Musk tweets, EV market sentiment\n")
  - Innovation news, Elon Musk tweets, EV market sentiment
cat("  - Model missing important drivers of Tesla returns\n\n")
  - Model missing important drivers of Tesla returns
cat("Suggestions for model improvement:\n")
Suggestions for model improvement:
cat("  - Fama-French 5-Factor Model (adds profitability, investment)\n")
  - Fama-French 5-Factor Model (adds profitability, investment)
cat("  - Industry-specific factors (EV sector, auto industry)\n")
  - Industry-specific factors (EV sector, auto industry)
cat("  - Sentiment/behavioral proxies\n")
  - Sentiment/behavioral proxies

8 Question 9: Momentum Factor (4-Factor Model)

8.1 Conceptual Background

The Carhart 4-Factor Model adds momentum to FF3:

\[R_i - R_f = \alpha + \beta (R_m - R_f) + s \cdot SMB + h \cdot HML + m \cdot MOM + \epsilon\]

MOM (Momentum): Winners minus Losers. Positive loading = stock exhibits momentum (past winners continue winning).

8.2 Run 4-Factor Regressions

# 4-Factor for Tesla
ff4_tsla <- lm(TSLA_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

# 4-Factor for Toyota
ff4_tm <- lm(TM_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

# 4-Factor for GM
ff4_gm <- lm(GM_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

8.3 4-Factor Results

8.3.1 Tesla 4-Factor

summary(ff4_tsla)

Call:
lm(formula = TSLA_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.31202 -0.10980 -0.01886  0.07254  0.74654 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.02887    0.01225   2.356  0.01959 *  
Mkt_RF       1.56086    0.30592   5.102 8.77e-07 ***
SMB          1.09062    0.49128   2.220  0.02772 *  
HML         -1.24738    0.37408  -3.334  0.00105 ** 
MOM         -0.31630    0.37491  -0.844  0.40001    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.1554 on 173 degrees of freedom
Multiple R-squared:  0.2594,    Adjusted R-squared:  0.2423 
F-statistic: 15.15 on 4 and 173 DF,  p-value: 1.229e-10

8.3.2 Toyota 4-Factor

summary(ff4_tm)

Call:
lm(formula = TM_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.13756 -0.03252 -0.00421  0.02489  0.17760 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.001423   0.004124   0.345    0.731    
Mkt_RF       0.611732   0.102952   5.942 1.51e-08 ***
SMB         -0.309356   0.165327  -1.871    0.063 .  
HML          0.093140   0.125889   0.740    0.460    
MOM         -0.147488   0.126166  -1.169    0.244    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.05229 on 173 degrees of freedom
Multiple R-squared:  0.2118,    Adjusted R-squared:  0.1936 
F-statistic: 11.63 on 4 and 173 DF,  p-value: 2.203e-08

8.3.3 GM 4-Factor

summary(ff4_gm)

Call:
lm(formula = GM_excess ~ Mkt_RF + SMB + HML + MOM, data = analysis_df)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.166238 -0.041550 -0.002429  0.035043  0.188050 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -0.003313   0.005212  -0.636  0.52584    
Mkt_RF       1.262524   0.130124   9.702  < 2e-16 ***
SMB          0.529927   0.208963   2.536  0.01210 *  
HML          0.526729   0.159116   3.310  0.00113 ** 
MOM         -0.183165   0.159466  -1.149  0.25230    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.06609 on 173 degrees of freedom
Multiple R-squared:  0.504, Adjusted R-squared:  0.4926 
F-statistic: 43.95 on 4 and 173 DF,  p-value: < 2.2e-16

8.4 Momentum Interpretation

Decision Rule (5% significance):

  • m > 0 and p < 0.05 → Positive momentum
  • m < 0 and p < 0.05 → Negative momentum (contrarian)
  • p > 0.05 → No significant momentum
cat("Momentum Factor (MOM) Results:\n\n")
Momentum Factor (MOM) Results:
# Tesla
cat("Tesla: m =", round(coef(ff4_tsla)[5], 2), ", p =", round(summary(ff4_tsla)$coefficients[5, 4], 3))
Tesla: m = -0.32 , p = 0.4
if (summary(ff4_tsla)$coefficients[5, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff4_tsla)[5] > 0, "Positive momentum", "Negative momentum"), "\n")
} else {
  cat(" → Not significant\n")
}
 → Not significant
# Toyota
cat("Toyota: m =", round(coef(ff4_tm)[5], 2), ", p =", round(summary(ff4_tm)$coefficients[5, 4], 3))
Toyota: m = -0.15 , p = 0.244
if (summary(ff4_tm)$coefficients[5, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff4_tm)[5] > 0, "Positive momentum", "Negative momentum"), "\n")
} else {
  cat(" → Not significant\n")
}
 → Not significant
# GM
cat("GM: m =", round(coef(ff4_gm)[5], 2), ", p =", round(summary(ff4_gm)$coefficients[5, 4], 3))
GM: m = -0.18 , p = 0.252
if (summary(ff4_gm)$coefficients[5, 4] < 0.05) {
  cat(" → ", ifelse(coef(ff4_gm)[5] > 0, "Positive momentum", "Negative momentum"), "\n")
} else {
  cat(" → Not significant\n")
}
 → Not significant
cat("\nConclusion: No significant momentum effect for any stock at 5% level.\n")

Conclusion: No significant momentum effect for any stock at 5% level.
cat("All stocks show negative (contrarian) loadings but statistically insignificant.\n")
All stocks show negative (contrarian) loadings but statistically insignificant.

8.5 4-Factor Summary Table

ff4_summary <- data.frame(
  Stock = c("Tesla", "Toyota", "GM"),
  Alpha = c(coef(ff4_tsla)[1], coef(ff4_tm)[1], coef(ff4_gm)[1]),
  Alpha_p = c(
    summary(ff4_tsla)$coefficients[1, 4],
    summary(ff4_tm)$coefficients[1, 4],
    summary(ff4_gm)$coefficients[1, 4]
  ),
  MOM = c(coef(ff4_tsla)[5], coef(ff4_tm)[5], coef(ff4_gm)[5]),
  MOM_p = c(
    summary(ff4_tsla)$coefficients[5, 4],
    summary(ff4_tm)$coefficients[5, 4],
    summary(ff4_gm)$coefficients[5, 4]
  ),
  R2 = c(
    summary(ff4_tsla)$r.squared,
    summary(ff4_tm)$r.squared,
    summary(ff4_gm)$r.squared
  )
)

ff4_summary %>%
  mutate(
    Alpha = paste0(round(Alpha * 100, 2), "%"),
    Alpha_p = round(Alpha_p, 3),
    MOM = round(MOM, 2),
    MOM_p = round(MOM_p, 3),
    R2 = paste0(round(R2 * 100, 1), "%")
  ) %>%
  kable(col.names = c("Stock", "Alpha", "α p-val", "MOM", "MOM p-val", "R²"))
Stock Alpha α p-val MOM MOM p-val
Tesla 2.89% 0.020 -0.32 0.400 25.9%
Toyota 0.14% 0.731 -0.15 0.244 21.2%
GM -0.33% 0.526 -0.18 0.252 50.4%

8.6 R² Improvement from Adding Momentum

cat("R² Comparison (FF3 vs 4-Factor):\n\n")
R² Comparison (FF3 vs 4-Factor):
cat("Tesla: FF3 =", round(summary(ff3_tsla)$r.squared * 100, 1), "% → 4F =", round(summary(ff4_tsla)$r.squared * 100, 1), "%\n")
Tesla: FF3 = 25.6 % → 4F = 25.9 %
cat("Toyota: FF3 =", round(summary(ff3_tm)$r.squared * 100, 1), "% → 4F =", round(summary(ff4_tm)$r.squared * 100, 1), "%\n")
Toyota: FF3 = 20.6 % → 4F = 21.2 %
cat("GM: FF3 =", round(summary(ff3_gm)$r.squared * 100, 1), "% → 4F =", round(summary(ff4_gm)$r.squared * 100, 1), "%\n")
GM: FF3 = 50 % → 4F = 50.4 %
cat("\nMinimal improvement from adding momentum factor.\n")

Minimal improvement from adding momentum factor.

9 Summary Table

final_summary <- data.frame(
  Stock = c("Tesla", "Toyota", "GM"),
  Classification = c("Cyclical", "Defensive", "Cyclical"),
  Beta_CAPM = c(round(coef(capm_tsla)[2], 2), round(coef(capm_tm)[2], 2), round(coef(capm_gm)[2], 2)),
  Size = c("Not sig", "Not sig", "Small-cap-like"),
  Value_Growth = c("Growth", "Not sig", "Value"),
  Alpha_FF3 = c(
    paste0(round(coef(ff3_tsla)[1] * 100, 2), "%"),
    paste0(round(coef(ff3_tm)[1] * 100, 2), "%"),
    paste0(round(coef(ff3_gm)[1] * 100, 2), "%")
  ),
  Alpha_Sig = c("Borderline", "No", "No"),
  Momentum = c("No", "No", "No"),
  R2_FF3 = c(
    paste0(round(summary(ff3_tsla)$r.squared * 100, 1), "%"),
    paste0(round(summary(ff3_tm)$r.squared * 100, 1), "%"),
    paste0(round(summary(ff3_gm)$r.squared * 100, 1), "%")
  )
)

kable(final_summary, col.names = c("Stock", "Type", "β (CAPM)", "Size", "Value/Growth", "α (FF3)", "α Sig?", "Momentum?", "R² (FF3)"))
Stock Type β (CAPM) Size Value/Growth α (FF3) α Sig? Momentum? R² (FF3)
Tesla Cyclical 1.84 Not sig Growth 2.76% Borderline No 25.6%
Toyota Defensive 0.60 Not sig Not sig 0.08% No No 20.6%
GM Cyclical 1.44 Small-cap-like Value -0.41% No No 50%

10 Key Takeaways

  1. Risk-adjusted performance (alpha) is more important than raw returns — Question 1-2 demonstrate this clearly.

  2. One year is not enough to prove skill — Idiosyncratic risk creates noise; need statistical significance over multiple periods.

  3. Tesla and GM are cyclical (β > 1); Toyota is defensive (β < 1) — Classification consistent across CAPM and FF3.

  4. Tesla exhibits growth stock characteristics — Negative HML loading (significant).

  5. GM behaves like a small-cap value stock — Positive SMB and HML loadings (both significant), despite large market cap.

  6. No significant alpha at strict 5% level — Tesla borderline; returns largely explained by factor exposures.

  7. GM best explained by systematic factors (R² = 55%) — Tesla worst explained (R² = 23%), dominated by firm-specific news.

  8. No significant momentum effect — All stocks show negative (contrarian) loadings but insignificant.


11 Appendix: Session Information

sessionInfo()
R version 4.4.0 (2024-04-24 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)

Matrix products: default


locale:
[1] LC_COLLATE=Chinese (Simplified)_China.utf8 
[2] LC_CTYPE=Chinese (Simplified)_China.utf8   
[3] LC_MONETARY=Chinese (Simplified)_China.utf8
[4] LC_NUMERIC=C                               
[5] LC_TIME=Chinese (Simplified)_China.utf8    

time zone: Asia/Shanghai
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] broom_1.0.10    knitr_1.50      lubridate_1.9.4 readr_2.1.6    
 [5] tidyr_1.3.1     dplyr_1.1.4     quantmod_0.4.28 TTR_0.24.4     
 [9] xts_0.14.1      zoo_1.8-14     

loaded via a namespace (and not attached):
 [1] vctrs_0.6.5       cli_3.6.5         rlang_1.1.6       xfun_0.54        
 [5] purrr_1.2.0       renv_1.0.7        generics_0.1.4    jsonlite_2.0.0   
 [9] glue_1.8.0        backports_1.5.0   htmltools_0.5.8.1 hms_1.1.4        
[13] rmarkdown_2.30    grid_4.4.0        tibble_3.3.0      evaluate_1.0.5   
[17] tzdb_0.5.0        fastmap_1.2.0     yaml_2.3.11       lifecycle_1.0.4  
[21] compiler_4.4.0    timechange_0.3.0  pkgconfig_2.0.3   rstudioapi_0.17.1
[25] lattice_0.22-6    digest_0.6.39     R6_2.6.1          tidyselect_1.2.1 
[29] pillar_1.11.1     curl_7.0.0        magrittr_2.0.4    withr_3.0.2      
[33] tools_4.4.0