Question 1. Single-Factor (Market) Model

Given:

alpha_hat  <- 0.0017
se_alpha   <- 0.0020
beta_hat   <- 0.98
se_beta    <- 0.17
R2         <- 0.50
mkt_premium <- 0.0070   # E[R_m - R_f] monthly
t_crit     <- 1.98

(a) t-statistic for β; test H₀: β = 0

t_beta <- beta_hat / se_beta
cat("t-statistic for beta:", round(t_beta, 4), "\n")
## t-statistic for beta: 5.765
cat("Critical value: ±", t_crit, "\n")
## Critical value: ± 1.98
cat("Reject H0 (beta = 0)?", abs(t_beta) > t_crit, "\n")
## Reject H0 (beta = 0)? TRUE

Formula: \(t_\beta = \hat{\beta} / SE(\hat{\beta}) = 0.98 / 0.17\)

Interpretation: β measures the fund’s sensitivity to market movements (systematic risk). A β of 0.98 means the fund moves approximately 1-for-1 with the market. Since |t| >> 1.98, we reject H₀; β is highly statistically significant.


(b) Test H₀: β = 1

t_beta1 <- (beta_hat - 1) / se_beta
cat("t-statistic for H0: beta = 1:", round(t_beta1, 4), "\n")
## t-statistic for H0: beta = 1: -0.1176
cat("Reject H0 (beta = 1)?", abs(t_beta1) > t_crit, "\n")
## Reject H0 (beta = 1)? FALSE

Formula: \(t = (\hat{\beta} - 1) / SE(\hat{\beta}) = (0.98 - 1) / 0.17\)

Interpretation: We fail to reject H₀: β = 1. The fund’s systematic risk is statistically indistinguishable from that of the market — it has average market risk.


(c) t-statistic for α (Jensen’s Alpha)

t_alpha <- alpha_hat / se_alpha
cat("t-statistic for alpha:", round(t_alpha, 4), "\n")
## t-statistic for alpha: 0.85
cat("Reject H0 (alpha = 0)?", abs(t_alpha) > t_crit, "\n")
## Reject H0 (alpha = 0)? FALSE

Formula: \(t_\alpha = \hat{\alpha} / SE(\hat{\alpha}) = 0.0017 / 0.0020\)

Conclusion: We fail to reject H₀: α = 0. Although α is positive (0.17% per month), it is not statistically significant at the 5% level. The marketing team’s claim of “positive risk-adjusted performance” is not statistically justified — the positive alpha may simply be due to sampling variation.


(d) Interpret R²

systematic_pct    <- R2 * 100
diversifiable_pct <- (1 - R2) * 100
cat("Systematic (market-explained) variation:", systematic_pct, "%\n")
## Systematic (market-explained) variation: 50 %
cat("Diversifiable (idiosyncratic) variation:", diversifiable_pct, "%\n")
## Diversifiable (idiosyncratic) variation: 50 %

Interpretation: \(R^2 = 0.50\) means 50% of the fund’s return variation is explained by market movements (systematic risk). The remaining 50% is idiosyncratic (diversifiable) risk that is specific to the fund’s holdings.


(e) CAPM-implied expected monthly excess return

E_excess_return <- beta_hat * mkt_premium
cat("CAPM-implied expected monthly excess return:", round(E_excess_return, 4),
    "(", round(E_excess_return * 100, 4), "% )\n")
## CAPM-implied expected monthly excess return: 0.0069 ( 0.686 % )

Formula: \(E[R_i - R_f] = \hat{\beta} \times E[R_m - R_f] = 0.98 \times 0.0070\)


Question 2. Fama–French Three-Factor Model

Given (144 monthly observations):

coefs <- c(alpha = 0.0029, b = 0.97, s = 0.75, h = -0.13)
ses   <- c(alpha = 0.0018, b = 0.08, s = 0.11, h = 0.13)

(f) t-statistics and significance

t_stats <- coefs / ses
sig     <- abs(t_stats) > t_crit

results <- data.frame(
  Coefficient = names(coefs),
  Estimate    = coefs,
  Std_Error   = ses,
  t_stat      = round(t_stats, 4),
  Significant = sig
)
print(results, row.names = FALSE)
##  Coefficient Estimate Std_Error t_stat Significant
##        alpha   0.0029    0.0018  1.611       FALSE
##            b   0.9700    0.0800 12.125        TRUE
##            s   0.7500    0.1100  6.818        TRUE
##            h  -0.1300    0.1300 -1.000       FALSE

Summary: MKT (b), SMB (s) are clearly significant. α is borderline — check its t-stat. HML (h) is not significant (|t| < 1.98).


(g) Investment style classification

cat("SMB loading (s):", coefs["s"], "→ Positive and large → SMALL-cap tilt\n")
## SMB loading (s): 0.75 → Positive and large → SMALL-cap tilt
cat("HML loading (h):", coefs["h"], "→ Negative (small)  → Slight GROWTH tilt\n")
## HML loading (h): -0.13 → Negative (small)  → Slight GROWTH tilt
cat("\nStyle: Small-cap, slight growth orientation\n")
## 
## Style: Small-cap, slight growth orientation

Interpretation: - s = 0.75 (positive, large): The fund tilts strongly toward small-cap stocks. Small stocks outperform large stocks in the SMB factor; a high positive loading means the fund behaves like small-cap holdings. - h = −0.13 (negative, insignificant): A negative HML loading indicates a slight growth tilt (growth stocks have low book-to-market). However, since this is not significant, the growth tilt is weak and uncertain.


(h) Intercept (alpha) interpretation

t_alpha_ff <- t_stats["alpha"]
cat("Alpha:", coefs["alpha"], "\n")
## Alpha: 0.0029
cat("t-statistic:", round(t_alpha_ff, 4), "\n")
## t-statistic: 1.611
cat("Significant at 5%?", abs(t_alpha_ff) > t_crit, "\n")
## Significant at 5%? FALSE

Interpretation: α = 0.0029 (≈ 0.29%/month). The t-statistic is 1.6111.

  • If |t| > 1.98: The manager does add statistically significant value beyond the three factor exposures — this is genuine skill (positive Jensen’s alpha in the FF framework).
  • If |t| ≤ 1.98: The alpha is not statistically distinguishable from zero; we cannot conclude the manager adds value beyond factor tilts.

(i) R² rise from 0.75 to 0.92; role of Adjusted R²

R2_capm <- 0.75
R2_ff   <- 0.92
adj_R2  <- 0.918
n       <- 144

cat("R2 increase:", R2_ff - R2_capm, "\n")
## R2 increase: 0.17
cat("Adjusted R2 (FF):", adj_R2, "\n")
## Adjusted R2 (FF): 0.918
# Manual adjusted R2 check for FF (k=3 predictors)
k_ff <- 3
adj_R2_check <- 1 - (1 - R2_ff) * (n - 1) / (n - k_ff - 1)
cat("Manually computed Adjusted R2:", round(adj_R2_check, 4), "\n")
## Manually computed Adjusted R2: 0.9183

Explanation: - The rise from R² = 0.75 (CAPM) to R² = 0.92 (FF) indicates the SMB and HML factors explain an additional 17% of the fund’s return variation. The fund has meaningful size and style exposures that CAPM misses entirely. - Why Adjusted R²? Adding any predictor, even a random noise variable, mechanically increases R². Adjusted R² penalizes for the number of predictors: \(\bar{R}^2 = 1 - \frac{(1-R^2)(n-1)}{n-k-1}\). Since Adjusted R² = 0.918 ≈ R² = 0.92, the added factors are genuinely informative, not just inflating fit.


Question 3. Logistic Regression for Market Direction

Given:

b0 <- -0.02;  b1 <- 5.4;  b2 <- -0.38
r_lag  <- 0.010
dVIX   <- 1.5

(j) Predicted probability and class

logit_val <- b0 + b1 * r_lag + b2 * dVIX
prob_up   <- 1 / (1 + exp(-logit_val))
pred_class <- ifelse(prob_up >= 0.5, "Up", "Down")

cat("Log-odds (logit):", round(logit_val, 4), "\n")
## Log-odds (logit): -0.536
cat("P(Up):", round(prob_up, 4), "\n")
## P(Up): 0.3691
cat("Predicted class (threshold = 0.5):", pred_class, "\n")
## Predicted class (threshold = 0.5): Down

Formula: \(\text{logit} = -0.02 + 5.4(0.010) + (-0.38)(1.5)\) \(P(Up) = \frac{1}{1 + e^{-\text{logit}}}\)


(k) Economic interpretation of β₁ and β₂

cat("beta1 =", b1, ": Positive → Momentum effect\n")
## beta1 = 5.4 : Positive → Momentum effect
cat("  A positive lagged return increases the log-odds of an Up day.\n")
##   A positive lagged return increases the log-odds of an Up day.
cat("  Captures short-term price momentum.\n\n")
##   Captures short-term price momentum.
cat("beta2 =", b2, ": Negative → Fear/volatility effect\n")
## beta2 = -0.38 : Negative → Fear/volatility effect
cat("  A rise in VIX (increasing fear) decreases the log-odds of an Up day.\n")
##   A rise in VIX (increasing fear) decreases the log-odds of an Up day.
cat("  Captures the negative relationship between volatility spikes and returns.\n")
##   Captures the negative relationship between volatility spikes and returns.

(l) Confusion matrix metrics

# Confusion matrix
TP <- 67   # Predicted Up,   Actual Up
FP <- 44   # Predicted Up,   Actual Down
FN <- 33   # Predicted Down, Actual Up
TN <- 56   # Predicted Down, Actual Down
N  <- 200

accuracy    <- (TP + TN) / N
sensitivity <- TP / (TP + FN)     # True Positive Rate for "Up"
specificity <- TN / (TN + FP)     # True Negative Rate for "Down"
precision   <- TP / (TP + FP)     # Precision for "Up" predictions

cat("Accuracy:    ", round(accuracy,    4), "\n")
## Accuracy:     0.615
cat("Sensitivity: ", round(sensitivity, 4), "\n")
## Sensitivity:  0.67
cat("Specificity: ", round(specificity, 4), "\n")
## Specificity:  0.56
cat("Precision:   ", round(precision,   4), "\n")
## Precision:    0.6036

Formulas: - Accuracy = (TP + TN) / N - Sensitivity (Recall for “Up”) = TP / (TP + FN) - Specificity = TN / (TN + FP) - Precision = TP / (TP + FP)


(m) Naïve majority-class rule

# Both classes = 100, so either class is majority; both equal → 50% naive accuracy
majority_class_count <- max(100, 100)   # Both are 100
naive_accuracy <- majority_class_count / N

cat("Naive rule accuracy:", round(naive_accuracy, 4), "\n")
## Naive rule accuracy: 0.5
cat("Model accuracy:     ", round(accuracy, 4), "\n")
## Model accuracy:      0.615
cat("Model beats naive rule?", accuracy > naive_accuracy, "\n")
## Model beats naive rule? TRUE

Why accuracy alone is inadequate for a trading system:

In a trading context, the cost of a false positive (going long on a Down day) and a false negative (missing an Up day) differ in economic terms. A strategy may have decent accuracy but poor risk-adjusted returns — e.g., it correctly predicts easy, low-return Up days but misses large Up days. A more economically relevant criterion is profit-weighted accuracy or the Sharpe ratio of the strategy implied by the predictions, which accounts for the magnitude of returns, not just directional correctness.


Question 4. Resampling and Regularization in a Backtest

Given: Mean monthly return = 0.70%, SD = 5.50%, n = 48 months

mu_monthly <- 0.0070   # 0.70%
sd_monthly <- 0.0550   # 5.50%
n_months   <- 48

(n) Monthly and annualized Sharpe ratio

sharpe_monthly  <- mu_monthly / sd_monthly
scaling_factor  <- sqrt(12)          # annualize monthly Sharpe
sharpe_annual   <- sharpe_monthly * scaling_factor

cat("Monthly Sharpe ratio:", round(sharpe_monthly, 4), "\n")
## Monthly Sharpe ratio: 0.1273
cat("Scaling factor:      ", round(scaling_factor, 4), "(= sqrt(12))\n")
## Scaling factor:       3.464 (= sqrt(12))
cat("Annualized Sharpe:   ", round(sharpe_annual,  4), "\n")
## Annualized Sharpe:    0.4409

Formula: \(SR_{monthly} = \bar{r}/\hat{\sigma}\); \(SR_{annual} = SR_{monthly} \times \sqrt{12}\)

The scaling factor is \(\sqrt{12}\) because returns are i.i.d. monthly: variance scales by 12, so SD scales by \(\sqrt{12}\), and Sharpe scales by \(\sqrt{12}\).


(o) Bootstrap procedure for Sharpe ratio SE

set.seed(42)

# Simulate a plausible monthly return series matching the given stats
set.seed(42)
r_sim <- rnorm(n_months, mean = mu_monthly, sd = sd_monthly)

# --- Standard i.i.d. bootstrap ---
B <- 10000
sharpe_boot_iid <- numeric(B)
for (i in 1:B) {
  boot_sample        <- sample(r_sim, size = n_months, replace = TRUE)
  sharpe_boot_iid[i] <- mean(boot_sample) / sd(boot_sample)
}
se_iid <- sd(sharpe_boot_iid)
cat("Bootstrap SE (i.i.d.):", round(se_iid, 6), "\n")
## Bootstrap SE (i.i.d.): 0.149
# --- Block bootstrap (stationary/circular) ---
# Appropriate for serially correlated return data
block_size <- 6   # e.g., 6-month blocks
sharpe_boot_block <- numeric(B)
for (i in 1:B) {
  n_blocks   <- ceiling(n_months / block_size)
  starts     <- sample(1:(n_months - block_size + 1), n_blocks, replace = TRUE)
  boot_idx   <- unlist(lapply(starts, function(s) s:(s + block_size - 1)))
  boot_sample <- r_sim[boot_idx[1:n_months]]
  sharpe_boot_block[i] <- mean(boot_sample) / sd(boot_sample)
}
se_block <- sd(sharpe_boot_block)
cat("Bootstrap SE (block, block_size=6):", round(se_block, 6), "\n")
## Bootstrap SE (block, block_size=6): 0.1686

Bootstrap procedure (step by step):

  1. Start with the original sample of 48 monthly returns.
  2. Draw B = 10,000 bootstrap samples of size 48 with replacement.
  3. For each bootstrap sample, compute the Sharpe ratio: \(SR^* = \bar{r}^* / \hat{\sigma}^*\).
  4. The bootstrap SE is the standard deviation of the 10,000 \(SR^*\) values.

Why ordinary i.i.d. bootstrap is inappropriate: Monthly returns often exhibit serial correlation (autocorrelation) — e.g., momentum effects, volatility clustering. The i.i.d. bootstrap destroys the time ordering, breaking any autocorrelation structure and understating the true variability of the Sharpe ratio.

Fix: Use the stationary block bootstrap (or circular block bootstrap). It resamples contiguous blocks of returns, preserving local autocorrelation structure.


(p) Lasso λ selection: minimum-CV vs. one-standard-error rule

lambda_min <- 0.030   # 14 factors, minimum CV error
lambda_1se <- 0.065   # 7 factors, one-SE rule

cat("lambda_min:", lambda_min, "→ 14 factors retained\n")
## lambda_min: 0.03 → 14 factors retained
cat("lambda_1se:", lambda_1se, "→  7 factors retained\n")
## lambda_1se: 0.065 →  7 factors retained
cat("\nRecommended: lambda_1se =", lambda_1se, "\n")
## 
## Recommended: lambda_1se = 0.065

Recommendation: Deploy \(\lambda = 0.065\) (one-standard-error rule, 7 factors).

Reasoning: - The minimum-CV solution (\(\lambda = 0.030\), 14 factors) minimizes in-sample cross-validation error, but with 60 candidate factors this risks overfitting — some of the 14 factors may be noise that happened to correlate with returns in the training window. - The one-SE rule selects the most regularized model whose CV error is within one standard error of the minimum. With only 7 factors, the model is simpler, more interpretable, and less likely to suffer data-snooping / look-ahead bias in a live backtest. - In finance, parsimony is especially valuable: each extra factor adds estimation error and transaction costs when rebalancing.


(q) Walk-forward (time-respecting) evaluation scheme

cat("Walk-forward cross-validation outline:\n\n")
## Walk-forward cross-validation outline:
cat("1. Training window: months 1 to T_train (e.g., first 36 months)\n")
## 1. Training window: months 1 to T_train (e.g., first 36 months)
cat("2. Fit lasso on training window; select lambda via nested CV within training only\n")
## 2. Fit lasso on training window; select lambda via nested CV within training only
cat("3. Generate predictions/signals for months T_train+1 to T_train+h (test window, e.g., 12 months)\n")
## 3. Generate predictions/signals for months T_train+1 to T_train+h (test window, e.g., 12 months)
cat("4. Record out-of-sample returns for those h months\n")
## 4. Record out-of-sample returns for those h months
cat("5. Expand (or roll) the training window forward by h months\n")
## 5. Expand (or roll) the training window forward by h months
cat("6. Repeat steps 2–5 until end of data\n")
## 6. Repeat steps 2–5 until end of data
cat("7. Aggregate all out-of-sample periods to compute the final Sharpe ratio\n\n")
## 7. Aggregate all out-of-sample periods to compute the final Sharpe ratio
cat("Why standard k-fold CV is unsafe:\n")
## Why standard k-fold CV is unsafe:
cat("  Random k-fold splits allow FUTURE data to appear in the training fold\n")
##   Random k-fold splits allow FUTURE data to appear in the training fold
cat("  and PAST data to appear in the validation fold — a look-ahead bias.\n")
##   and PAST data to appear in the validation fold — a look-ahead bias.
cat("  This artificially inflates performance: the model has 'seen' the future.\n")
##   This artificially inflates performance: the model has 'seen' the future.
cat("  Walk-forward CV strictly enforces that training always precedes testing in time.\n")
##   Walk-forward CV strictly enforces that training always precedes testing in time.

Walk-Forward Scheme (illustrated):

total_months <- 48
train_start  <- 1
train_end    <- 36
step         <- 4   # expand by 4 months each fold

fold <- 1
while ((train_end + step) <= total_months) {
  test_start <- train_end + 1
  test_end   <- min(train_end + step, total_months)
  cat(sprintf("Fold %d: Train [%d–%d], Test [%d–%d]\n",
              fold, train_start, train_end, test_start, test_end))
  train_end <- test_end
  fold <- fold + 1
}
## Fold 1: Train [1–36], Test [37–40]
## Fold 2: Train [1–40], Test [41–44]
## Fold 3: Train [1–44], Test [45–48]

Why random k-fold CV is unsafe for time-series: Standard k-fold randomly assigns observations to folds, so a model trained on data from month 40 can be validated on month 10 — effectively training on the future. This introduces look-ahead bias, inflating the apparent Sharpe ratio. In a real deployment, such future information is unavailable, so the backtest is an unreliable estimate of live performance. ```