library(xts)
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
library(tibble)
# Simulate test returns for 3 assets over 100 days
set.seed(123)
dates <- seq(as.Date("2024-01-01"), by = "days", length.out = 100)
wide_returns <- tibble(
date = dates,
SPY = rnorm(100, 0.0005, 0.01),
QQQ = rnorm(100, 0.0007, 0.012),
IWM = rnorm(100, 0.0004, 0.015)
)
# Convert to xts
rets <- wide_returns[-1] # remove date
rets_xts <- xts(rets, order.by = wide_returns$date)
library(PortfolioAnalytics)
## Loading required package: foreach
## Loading required package: PerformanceAnalytics
##
## Attaching package: 'PerformanceAnalytics'
## The following object is masked from 'package:graphics':
##
## legend
## Registered S3 method overwritten by 'PortfolioAnalytics':
## method from
## print.constraint ROI
library(PerformanceAnalytics)
library(ROI)
## ROI: R Optimization Infrastructure
## Registered solver plugins: nlminb, symphony, quadprog.
## Default solver: auto.
##
## Attaching package: 'ROI'
## The following objects are masked from 'package:PortfolioAnalytics':
##
## is.constraint, objective
library(ROI.plugin.quadprog)
funds <- colnames(rets_xts)
pspec <- portfolio.spec(assets = funds)
pspec <- add.constraint(pspec, type = "full_investment")
pspec <- add.constraint(pspec, type = "long_only")
pspec <- add.objective(pspec, type = "risk", name = "StdDev")
pspec <- add.objective(pspec, type = "return", name = "mean")
library(quantmod)
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
library(PerformanceAnalytics)
library(tseries)
symbols <- c("SPY", "QQQ", "IWM")
getSymbols(symbols, from = "2020-01-01", to = "2024-12-31")
## [1] "SPY" "QQQ" "IWM"
rets <- na.omit(Return.calculate(merge(Cl(SPY), Cl(QQQ), Cl(IWM))))
colnames(rets) <- symbols
target_returns <- seq(min(colMeans(rets)), max(colMeans(rets)), length.out = 25)
frontier <- lapply(target_returns, function(target) {
portfolio.optim(x = rets, pm = target)
})
means <- sapply(frontier, function(x) x$pm)
risks <- sapply(frontier, function(x) sqrt(x$ps))
jpeg("ef_plot.jpg")
plot(risks, means, type = "l", col = "blue", lwd = 2,
xlab = "Risk (Standard Deviation)", ylab = "Expected Return",
main = "Efficient Frontier: SPY, QQQ, IWM")
points(risks, means, pch = 16, col = "red")
dev.off()
## png
## 2
knitr::include_graphics("ef_plot.jpg")

library(quantmod)
getSymbols("AAPL", from = "2023-01-01", to = "2024-01-01")
## [1] "AAPL"
pre_event <- window(Cl(AAPL), end = as.Date("2023-07-31"))
post_event <- window(Cl(AAPL), start = as.Date("2023-08-02"))
pre_return <- dailyReturn(pre_event)
post_return <- dailyReturn(post_event)
summary(pre_return)
## Index daily.returns
## Min. :2023-01-03 Min. :-0.026680
## 1st Qu.:2023-02-23 1st Qu.:-0.005863
## Median :2023-04-17 Median : 0.002019
## Mean :2023-04-16 Mean : 0.003217
## 3rd Qu.:2023-06-07 3rd Qu.: 0.010341
## Max. :2023-07-31 Max. : 0.046927
summary(post_return)
## Index daily.returns
## Min. :2023-08-02 Min. :-0.0480201
## 1st Qu.:2023-09-08 1st Qu.:-0.0073216
## Median :2023-10-16 Median : 0.0012667
## Mean :2023-10-15 Mean : 0.0000746
## 3rd Qu.:2023-11-21 3rd Qu.: 0.0079049
## Max. :2023-12-29 Max. : 0.0219489
library(tidyquant)
## ── Conflicts ────────────────────────────────────────── tidyquant_conflicts() ──
## ✖ zoo::as.Date() masks base::as.Date()
## ✖ zoo::as.Date.numeric() masks base::as.Date.numeric()
## ✖ PerformanceAnalytics::legend() masks graphics::legend()
## ✖ quantmod::summary() masks base::summary()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
##
## ######################### Warning from 'xts' package ##########################
## # #
## # The dplyr lag() function breaks how base R's lag() function is supposed to #
## # work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or #
## # source() into this session won't work correctly. #
## # #
## # Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
## # conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop #
## # dplyr from breaking base R's lag() function. #
## # #
## # Code in packages is not affected. It's protected by R's namespace mechanism #
## # Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning. #
## # #
## ###############################################################################
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:xts':
##
## first, last
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidyr)
symbols <- c("SPY", "QQQ", "IWM")
prices <- tq_get(symbols, from = "2023-01-01", to = "2024-01-01")
returns <- prices %>%
group_by(symbol) %>%
tq_transmute(select = adjusted, mutate_fun = periodReturn, period = "monthly")
wide_returns <- returns %>%
pivot_wider(names_from = symbol, values_from = monthly.returns)
cor(wide_returns[-1], use = "complete.obs")
## SPY QQQ IWM
## SPY 1.0000000 0.8772802 0.8268346
## QQQ 0.8772802 1.0000000 0.6647968
## IWM 0.8268346 0.6647968 1.0000000
portfolios <- data.frame(
Portfolio = c("W", "X", "Z", "Y"),
Return = c(15, 12, 5, 9),
StdDev = c(36, 15, 7, 21)
)
library(ggplot2)
ggplot(portfolios, aes(x = StdDev, y = Return, label = Portfolio)) +
geom_point(color = "blue", size = 3) +
geom_text(vjust = -0.8) +
labs(title = "CFA 7.4 – Portfolio Returns vs Risk",
x = "Standard Deviation (%)",
y = "Expected Return (%)") +
theme_minimal()

beta_A <- 1.0
beta_B <- 1.0
rf <- 0.05
market_premium <- 0.08
exp_return_A <- rf + beta_A * market_premium
exp_return_B <- rf + beta_B * market_premium
paste0("Expected Return of A = ", round(exp_return_A * 100, 2), "%\n",
"Expected Return of B = ", round(exp_return_B * 100, 2), "%")
## [1] "Expected Return of A = 13%\nExpected Return of B = 13%"
risk_free_rate <- 0.05
market_risk_premium <- 0.08
beta <- 1.5
expected_return <- risk_free_rate + beta * market_risk_premium
paste0("Expected return = ", round(expected_return * 100, 2), "%")
## [1] "Expected return = 17%"
library(quantmod)
library(PerformanceAnalytics)
getSymbols(c("AAPL", "^GSPC"), from = "2023-01-01", to = "2024-01-01")
## [1] "AAPL" "GSPC"
stock_ret <- dailyReturn(Cl(AAPL))
market_ret <- dailyReturn(Cl(GSPC))
model <- lm(stock_ret ~ market_ret)
summary(model)
##
## Call:
## lm(formula = stock_ret ~ market_ret)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.042963 -0.004653 0.000441 0.004985 0.035036
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0007919 0.0005505 1.439 0.152
## market_ret 1.1036341 0.0665136 16.593 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.008651 on 248 degrees of freedom
## Multiple R-squared: 0.5261, Adjusted R-squared: 0.5242
## F-statistic: 275.3 on 1 and 248 DF, p-value: < 2.2e-16
r_squared <- summary(model)$r.squared
std_error <- summary(model)$sigma
r_squared
## [1] 0.5260981
std_error
## [1] 0.008650579
model_beta <- coef(model)[2]
broker_a <- 0.62
broker_b <- 0.71
c(model_beta = model_beta, broker_a = broker_a, broker_b = broker_b)
## model_beta.market_ret broker_a broker_b
## 1.103634 0.620000 0.710000
beta_value <- coef(model)[2]
total_risk <- sd(stock_ret)
market_risk <- sd(market_ret)
risk_table <- data.frame(
Beta = beta_value,
Std_Dev = total_risk,
Market_StdDev = market_risk
)
risk_table
## Beta Std_Dev Market_StdDev
## market_ret 1.103634 0.01254085 0.008242053
rf <- 0.03
r_return <- 0.11
r_sd <- 0.10
r_beta <- 0.5
m_return <- 0.14
m_sd <- 0.12
m_beta <- 1.0
sharpe_r <- (r_return - rf) / r_sd
sharpe_m <- (m_return - rf) / m_sd
treynor_r <- (r_return - rf) / r_beta
treynor_m <- (m_return - rf) / m_beta
data.frame(
Portfolio = c("R", "S&P 500"),
Sharpe = c(sharpe_r, sharpe_m),
Treynor = c(treynor_r, treynor_m)
)
## Portfolio Sharpe Treynor
## 1 R 0.8000000 0.16
## 2 S&P 500 0.9166667 0.11
portfolio_data <- data.frame(
Portfolio = c("R", "S&P 500"),
Return = c(0.11, 0.14),
StdDev = c(0.10, 0.12),
Beta = c(0.5, 1.0)
)
library(ggplot2)
ggplot(portfolio_data, aes(x = StdDev, y = Return, label = Portfolio)) +
geom_point(size = 3, color = "blue") +
geom_text(vjust = -1) +
labs(title = "Risk vs Return Plot",
x = "Standard Deviation (Risk)",
y = "Expected Return") +
theme_minimal()

install.packages("ggplot2")
## Installing package into '/cloud/lib/x86_64-pc-linux-gnu-library/4.4'
## (as 'lib' is unspecified)
install.packages("dplyr")
## Installing package into '/cloud/lib/x86_64-pc-linux-gnu-library/4.4'
## (as 'lib' is unspecified)
library(ggplot2)
library(dplyr)
rf <- 0.03
m_return <- 0.10
beta_vals <- seq(0, 1.5, by = 0.1)
sml_returns <- rf + beta_vals * (m_return - rf)
sml_df <- data.frame(
Beta = beta_vals,
ExpectedReturn = sml_returns
)
portfolio_data <- data.frame(
Portfolio = c("A", "B", "C"),
Beta = c(0.5, 1.0, 1.3),
Return = c(0.06, 0.10, 0.13)
)
ggplot(sml_df, aes(x = Beta, y = ExpectedReturn)) +
geom_line(color = "darkgreen", size = 1.2) +
geom_point(data = portfolio_data, aes(x = Beta, y = Return), color = "blue", size = 3) +
geom_text(data = portfolio_data, aes(x = Beta, y = Return, label = Portfolio), vjust = -1) +
labs(title = "Security Market Line (SML)",
x = "Beta",
y = "Expected Return") +
theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

expected_return <- 0.12
beta_ip <- 1
beta_ir <- 0.5
expected_ip <- 0.03
actual_ip <- 0.05
expected_ir <- 0.05
actual_ir <- 0.08
revised_return <- expected_return +
beta_ip * (actual_ip - expected_ip) +
beta_ir * (actual_ir - expected_ir)
revised_return
## [1] 0.155
library(MASS)
##
## Attaching package: 'MASS'
## The following object is masked from 'package:dplyr':
##
## select
betas <- matrix(c(1.5, 2.0,
2.2, -0.2), nrow = 2, byrow = TRUE)
returns <- c(0.31 - 0.06, 0.27 - 0.06) # Subtract rf
lambdas <- ginv(betas) %*% returns
lambdas
## [,1]
## [1,] 0.10
## [2,] 0.05
rf <- 0.06
beta1_A <- 1.5
beta2_A <- 2.0
expected_A <- rf + beta1_A * lambdas[1] + beta2_A * lambdas[2]
beta1_B <- 2.2
beta2_B <- -0.2
expected_B <- rf + beta1_B * lambdas[1] + beta2_B * lambdas[2]
data.frame(Portfolio = c("A", "B"),
GivenReturn = c(0.31, 0.27),
APT_Return = c(expected_A, expected_B))
## Portfolio GivenReturn APT_Return
## 1 A 0.31 0.31
## 2 B 0.27 0.27
emh_types <- data.frame(
Type = c("Weak-form", "Semi-strong form", "Strong-form"),
Information_Used = c(
"Past price and volume data",
"All publicly available information (incl. financials, news)",
"All information, both public and private (insider info)"
),
Implication = c(
"Technical analysis is useless",
"Neither technical nor fundamental analysis consistently helps",
"Even insiders can't consistently outperform"
)
)
emh_types
## Type Information_Used
## 1 Weak-form Past price and volume data
## 2 Semi-strong form All publicly available information (incl. financials, news)
## 3 Strong-form All information, both public and private (insider info)
## Implication
## 1 Technical analysis is useless
## 2 Neither technical nor fundamental analysis consistently helps
## 3 Even insiders can't consistently outperform
set.seed(42)
n_days <- 100
returns <- rnorm(n_days, mean = 0.0005, sd = 0.01) # Simulate daily returns
price <- cumprod(1 + returns) * 100 # Starting from 100
plot(price, type = "l", col = "blue", lwd = 2,
main = "Simulated Stock Price (Random Walk)",
xlab = "Day", ylab = "Price")

set.seed(123)
pre_event <- rnorm(10, 0.0005, 0.01)
event_impact <- c(0.05) # Positive shock
post_event <- rnorm(10, 0.0005, 0.01)
price <- cumsum(c(pre_event, event_impact, post_event)) + 100
plot(price, type = "l", col = "darkgreen", lwd = 2,
main = "Stock Price Response to Earnings Surprise",
xlab = "Time (Day)", ylab = "Price")
abline(v = 11, col = "red", lty = 2)
text(11, price[11] + 0.5, "Event Day", pos = 4)

set.seed(111)
days <- 250
active <- cumprod(1 + rnorm(days, 0.0004, 0.012)) * 100
passive <- cumprod(1 + rnorm(days, 0.0005, 0.01)) * 100
plot(passive, type = "l", col = "black", lwd = 2,
ylim = range(c(active, passive)),
ylab = "Value", xlab = "Days", main = "Active vs. Passive Fund")
lines(active, col = "blue", lwd = 2)
legend("topleft", legend = c("Passive Index", "Active Fund"),
col = c("black", "blue"), lwd = 2)

emh_summary <- data.frame(
Strategy = c("Technical Analysis", "Fundamental Analysis", "Index Investing", "Insider Trading"),
WeakForm = c("No advantage", "May help", "Good", "Possible advantage"),
SemiStrongForm = c("No advantage", "No advantage", "Good", "Possible advantage"),
StrongForm = c("No advantage", "No advantage", "Good", "No advantage")
)
emh_summary
## Strategy WeakForm SemiStrongForm StrongForm
## 1 Technical Analysis No advantage No advantage No advantage
## 2 Fundamental Analysis May help No advantage No advantage
## 3 Index Investing Good Good Good
## 4 Insider Trading Possible advantage Possible advantage No advantage
emh_vs_behavioral <- data.frame(
Assumption = c("Investor Rationality", "No Arbitrage"),
EMH_View = c("Investors are rational and utility-maximizing",
"Mispricings are quickly exploited by arbitrageurs"),
Behavioral_View = c("Investors often act irrationally due to biases and heuristics",
"Limits to arbitrage exist (cost, risk, sentiment-driven bubbles)")
)
emh_vs_behavioral
## Assumption EMH_View
## 1 Investor Rationality Investors are rational and utility-maximizing
## 2 No Arbitrage Mispricings are quickly exploited by arbitrageurs
## Behavioral_View
## 1 Investors often act irrationally due to biases and heuristics
## 2 Limits to arbitrage exist (cost, risk, sentiment-driven bubbles)
library(ggplot2)
gain_loss <- seq(-50, 50, by = 1)
utility <- ifelse(gain_loss >= 0, gain_loss^0.88, -2.25 * (-gain_loss)^0.88)
loss_aversion <- data.frame(gain_loss, utility)
ggplot(loss_aversion, aes(x = gain_loss, y = utility)) +
geom_line(color = "red", size = 1.2) +
labs(title = "Loss Aversion Curve",
x = "Gain or Loss",
y = "Perceived Value / Utility") +
theme_minimal()

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ readr 2.1.5
## ✔ lubridate 1.9.4 ✔ stringr 1.5.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ purrr::accumulate() masks foreach::accumulate()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::first() masks xts::first()
## ✖ dplyr::lag() masks stats::lag()
## ✖ dplyr::last() masks xts::last()
## ✖ MASS::select() masks dplyr::select()
## ✖ purrr::when() masks foreach::when()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
stock_data <- data.frame(
market_excess = c(0.01, 0.015, -0.005, 0.02, 0.01, 0.025, -0.01, 0.005, 0.015, -0.005),
stock_A_excess = c(0.005, 0.01, -0.01, 0.015, 0.005, 0.03, -0.015, 0.002, 0.012, -0.008)
)
model_A <- lm(stock_A_excess ~ market_excess, data = stock_data)
summary(model_A)
##
## Call:
## lm(formula = stock_A_excess ~ market_excess, data = stock_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.0031074 -0.0018512 -0.0002231 0.0007479 0.0062645
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.004405 0.001134 -3.884 0.00465 **
## market_excess 1.125620 0.083385 13.499 8.7e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.002901 on 8 degrees of freedom
## Multiple R-squared: 0.9579, Adjusted R-squared: 0.9527
## F-statistic: 182.2 on 1 and 8 DF, p-value: 8.701e-07
hypotheses <- data.frame(
Null = "γ1 > 0: Higher beta → Higher expected return (CAPM holds)",
Alt = "γ1 = 0: No relation between beta and return (CAPM fails)",
Intercept = "γ0 ≈ 0 if excess return is used"
)
hypotheses
## Null
## 1 γ1 > 0: Higher beta → Higher expected return (CAPM holds)
## Alt
## 1 γ1 = 0: No relation between beta and return (CAPM fails)
## Intercept
## 1 γ0 ≈ 0 if excess return is used
beta_values <- c(-0.2, 0.1, 0.5, 0.7, 0.9, 1.0, 1.1, 1.3, 1.5)
avg_excess_returns <- c(0.002, 0.004, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.013)
sml_data <- data.frame(beta = beta_values, return = avg_excess_returns)
sml_model <- lm(return ~ beta, data = sml_data)
summary(sml_model)
##
## Call:
## lm(formula = return ~ beta, data = sml_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.0006034 -0.0002227 -0.0000804 0.0002082 0.0006811
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0030303 0.0002475 12.25 5.55e-06 ***
## beta 0.0061924 0.0002667 23.22 6.97e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.0004182 on 7 degrees of freedom
## Multiple R-squared: 0.9872, Adjusted R-squared: 0.9854
## F-statistic: 539.3 on 1 and 7 DF, p-value: 6.967e-08
ggplot(sml_data, aes(x = beta, y = return)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE, col = "blue") +
labs(title = "Security Market Line (SML)", x = "Beta", y = "Average Excess Return") +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

cat("Higher beta stocks tend to offer higher returns.\n")
## Higher beta stocks tend to offer higher returns.
cat("R² of second-pass regression indicates strength of CAPM fit.\n")
## R² of second-pass regression indicates strength of CAPM fit.
cat("Stock H and I with high betas performed best, A underperformed.\n")
## Stock H and I with high betas performed best, A underperformed.
sml_data$portfolio <- cut(sml_data$beta, breaks = 3, labels = c("Low", "Medium", "High"))
portfolio_summary <- sml_data %>%
group_by(portfolio) %>%
summarize(beta = mean(beta), return = mean(return))
sml_portfolio_model <- lm(return ~ beta, data = portfolio_summary)
summary(sml_portfolio_model)
##
## Call:
## lm(formula = return ~ beta, data = portfolio_summary)
##
## Residuals:
## 1 2 3
## 0.0001518 -0.0003687 0.0002169
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.0031495 0.0004083 7.714 0.0821 .
## beta 0.0060274 0.0005009 12.033 0.0528 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.0004539 on 1 degrees of freedom
## Multiple R-squared: 0.9931, Adjusted R-squared: 0.9863
## F-statistic: 144.8 on 1 and 1 DF, p-value: 0.05278
ggplot(portfolio_summary, aes(x = beta, y = return)) +
geom_point(size = 3) +
geom_smooth(method = "lm", se = FALSE, color = "pink") +
labs(title = "SML with Portfolios", x = "Portfolio Beta", y = "Portfolio Excess Return") +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

cat("Roll's critique argues CAPM cannot be tested because the true market portfolio is unobservable.\n")
## Roll's critique argues CAPM cannot be tested because the true market portfolio is unobservable.
cat("Using a proxy like S&P 500 introduces benchmark error.\n")
## Using a proxy like S&P 500 introduces benchmark error.
cat("Thus, CAPM test results may reflect errors in the benchmark, not model failure.\n")
## Thus, CAPM test results may reflect errors in the benchmark, not model failure.
portfolio_summary$sd <- c(0.07, 0.09, 0.12)
market_return <- 0.009
market_sd <- 0.10
rf_rate <- 0.002
cml_slope <- (market_return - rf_rate) / market_sd
cml_x <- seq(0, 0.15, 0.01)
cml_y <- rf_rate + cml_slope * cml_x
plot(cml_x, cml_y, type = "l", col = "pink", lwd = 2,
main = "Capital Market Line", xlab = "Standard Deviation", ylab = "Expected Return")
points(portfolio_summary$sd, portfolio_summary$return, pch = 19, col = "purple")
text(portfolio_summary$sd, portfolio_summary$return + 0.001,
labels = portfolio_summary$portfolio, pos = 3)
points(market_sd, market_return, col = "red", pch = 17)
legend("topleft", legend = c("CML", "Portfolios", "Market"), col = c("pink", "purple", "red"),
lty = c(1, NA, NA), pch = c(NA, 19, 17))

cat("Portfolio performance is typically evaluated by comparing actual returns with expected returns predicted by a benchmark (e.g., CAPM using S&P 500).\n")
## Portfolio performance is typically evaluated by comparing actual returns with expected returns predicted by a benchmark (e.g., CAPM using S&P 500).
cat("The benchmark portfolio is assumed to represent the true 'market portfolio', used to derive expected returns based on beta.\n")
## The benchmark portfolio is assumed to represent the true 'market portfolio', used to derive expected returns based on beta.
cat("Richard Roll argued that the CAPM is untestable because the true market portfolio (which includes all assets: real estate, human capital, etc.) is unobservable.\n")
## Richard Roll argued that the CAPM is untestable because the true market portfolio (which includes all assets: real estate, human capital, etc.) is unobservable.
cat("Therefore, using a proxy like the S&P 500 introduces 'benchmark error' — any test of the CAPM may be invalid if the benchmark isn't the true market.\n")
## Therefore, using a proxy like the S&P 500 introduces 'benchmark error' — any test of the CAPM may be invalid if the benchmark isn't the true market.
library(ggplot2)
df <- data.frame(
Beta = c(0.5, 1.0, 1.5),
Measured_Return = c(0.05, 0.08, 0.11),
True_Return = c(0.03, 0.08, 0.13)
)
ggplot(df, aes(x = Beta)) +
geom_line(aes(y = Measured_Return), color = "blue", size = 1.2) +
geom_line(aes(y = True_Return), color = "red", size = 1.2, linetype = "dashed") +
geom_point(aes(y = Measured_Return), color = "blue") +
geom_point(aes(y = True_Return), color = "red") +
labs(title = "Measured vs. True Security Market Line",
y = "Expected Return", x = "Beta") +
theme_minimal() +
annotate("text", x = 1.2, y = 0.115, label = "Measured SML", color = "blue") +
annotate("text", x = 1.2, y = 0.135, label = "True SML", color = "red")

cat("If a manager is judged superior using multiple benchmarks (Dow Jones, S&P 500, NYSE), it increases confidence.\n")
## If a manager is judged superior using multiple benchmarks (Dow Jones, S&P 500, NYSE), it increases confidence.
cat("However, all of these may still be poor proxies for the true market portfolio.\n")
## However, all of these may still be poor proxies for the true market portfolio.
cat("So while consensus helps, Roll’s critique still applies — true skill is hard to prove without the true market.\n")
## So while consensus helps, Roll’s critique still applies — true skill is hard to prove without the true market.
cat("Position: CAPM is still useful despite benchmark errors.\n")
## Position: CAPM is still useful despite benchmark errors.
cat("Roll’s critique shows implementation challenges, not theoretical flaws.\n")
## Roll’s critique shows implementation challenges, not theoretical flaws.
cat("Using diversified indexes as proxies provides practical value for investment decisions and comparisons.\n")
## Using diversified indexes as proxies provides practical value for investment decisions and comparisons.
cat("Thus, CAPM should be refined, not scrapped.\n")
## Thus, CAPM should be refined, not scrapped.