Chapter 3 · Section 3 practice
From Prices to a CAPM Regression
Work from question 1 to 10. Edit your Python box, click Run Code, review the result, then click Submit attempt. Reference answers unlock after all ten attempts are submitted. This records completion, not correctness. Each box runs independently. Progress is saved in this browser.
1. Interpret positive alpha
Warm-up. For illustrative daily alpha=0.001, beta=1.5, market excess=-0.01 and RF=0.0002, print the fitted total return in percent. Explain why positive alpha can coexist with a loss.
Write your answer, then run it.
2. Read alpha
Warm-up. Print daily alpha as a decimal, rounded to six places.
Write your answer, then run it.
3. Interpret financial slopes
Warm-up. For beta=0.6 and beta=1.5, print the fitted excess-return changes when market excess rises by one percentage point; describe defensive and aggressive sensitivity.
Write your answer, then run it.
4. Check alignment
Build your skills. Print whether train and test share any dates.
Write your answer, then run it.
5. Read an excess return
Build your skills. Print the first NVDA return, RF, and their difference.
Write your answer, then run it.
6. Predict at zero market excess
Build your skills. Predict excess return when Market is zero.
Write your answer, then run it.
7. Predict a positive scenario
Build your skills. Predict excess return at Market=0.01.
Write your answer, then run it.
8. Recover total return
Challenge. Predict at Market=0.01 and add RF=0.0002; print total return as a percentage.
Write your answer, then run it.
9. Compare two market scenarios
Challenge. Predict at -1% and +1% market excess return; print their difference in percentage points.
Write your answer, then run it.
10. Separate a conditional scenario from a forecast
Challenge. Print a forecast-scope sentence after calculating beta.
Write your answer, then run it.
Reference answers are locked until all 10 attempts are submitted.
1. Interpret positive alpha
alpha, beta, market, rf = 0.001, 1.5, -0.01, 0.0002
print(round((rf + alpha + beta * market) * 100, 2))
print(
"Positive alpha is above the beta-adjusted benchmark, but a negative market contribution can still produce a loss."
)Expected output
-1.38
Positive alpha is above the beta-adjusted benchmark, but a negative market contribution can still produce a loss.A positive historical estimate is not a guarantee of positive or future returns.
2. Read alpha
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
print(round(model.params["const"], 6))Expected output
0.003623Daily alpha is measured in the same decimal-return units as Y.
3. Interpret financial slopes
market_change = 0.01
print(0.6 * market_change * 100, 1.5 * market_change * 100)
print(
"Beta 0.6 is defensive positive sensitivity; beta 1.5 is aggressive positive sensitivity."
)Expected output
0.6 1.5
Beta 0.6 is defensive positive sensitivity; beta 1.5 is aggressive positive sensitivity.The outputs are percentage-point changes, not guaranteed realised stock returns.
4. Check alignment
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
print(len(train.index.intersection(test.index)) == 0)Expected output
TrueDisjoint date indices prevent overlapping observations.
5. Read an excess return
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
row = train.iloc[0]
print(round(row["NVDA"], 6), round(row["RF"], 6))
print(round(row["NVDA"] - row["RF"], 6))Expected output
0.030318 0.0002
0.030118Subtract a risk-free return measured over the same daily period.
6. Predict at zero market excess
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
new = pd.DataFrame({"const": [1], "Market": [0]})
print(round(model.predict(new).iloc[0], 6))Expected output
0.003623The fitted value at zero X is alpha.
7. Predict a positive scenario
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
new = pd.DataFrame({"const": [1], "Market": [0.01]})
print(round(model.predict(new).iloc[0], 6))Expected output
0.0269440.01 is a 1% market excess-return scenario.
8. Recover total return
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
new = pd.DataFrame({"const": [1], "Market": [0.01]})
excess = model.predict(new).iloc[0]
print(round((excess + 0.0002) * 100, 3))Expected output
2.714Add the scenario risk-free return after predicting excess return.
9. Compare two market scenarios
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
new = pd.DataFrame({"const": [1, 1], "Market": [-0.01, 0.01]})
predicted = model.predict(new)
print(round((predicted.iloc[1] - predicted.iloc[0]) * 100, 3))Expected output
4.664A two-percentage-point X difference produces two times beta percentage points in fitted Y.
10. Separate a conditional scenario from a forecast
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
X = sm.add_constant(train[["Market"]])
model = sm.OLS(train["Excess"], X).fit()
print(round(model.params["Market"], 3))
print(
"The prediction is conditional on the supplied market return, which tomorrow is unknown."
)Expected output
2.332
The prediction is conditional on the supplied market return, which tomorrow is unknown.This prevents a contemporaneous return regression being sold as a stand-alone tomorrow forecast.