Instructions Before You Knit

asset_a <- "SPY"
asset_b <- "GLD"
asset_c <- "TLT"

symbols <- c(asset_a, asset_b, asset_c)
analysis_start <- "2020-01-01"
analysis_end <- "2026-02-17"

Data Download And Alignment

prices <- get_adjusted_prices(
  symbols = symbols,
  from = analysis_start,
  to = analysis_end
)

returns <- simple_returns(prices)

price_summary <- data.frame(
  asset = colnames(prices),
  first_date = as.character(start(prices)),
  last_date = as.character(end(prices)),
  observations = nrow(prices)
)

kable(price_summary, caption = "Aligned adjusted closing prices")
Aligned adjusted closing prices
asset first_date last_date observations
SPY 2020-01-02 2026-02-13 1538
GLD 2020-01-02 2026-02-13 1538
TLT 2020-01-02 2026-02-13 1538
saveRDS(prices, "aligned_prices.rds")
saveRDS(returns, "simple_returns.rds")

1. Asset A Analysis

1(a) Simple Daily Returns

asset_a_returns <- returns[, asset_a]
kable(head(data.frame(date = index(asset_a_returns), ret = coredata(asset_a_returns)), 10), digits = 6)
date SPY
2020-01-03 -0.007572
2020-01-06 0.003815
2020-01-07 -0.002812
2020-01-08 0.005330
2020-01-09 0.006781
2020-01-10 -0.002878
2020-01-13 0.006877
2020-01-14 -0.001525
2020-01-15 0.002260
2020-01-16 0.008319

1(b) Statistical Features Of Asset A Returns

asset_a_stats <- sample_features(asset_a_returns)
kable(round(asset_a_stats, 6), caption = "Sample features for Asset A simple returns")
Sample features for Asset A simple returns
mean sd skewness kurtosis min max
0.000624 0.012985 -0.263583 13.10959 -0.109424 0.105019
autoplot_data <- data.frame(
  date = index(asset_a_returns),
  ret = as.numeric(asset_a_returns)
)

ggplot(autoplot_data, aes(x = date, y = ret)) +
  geom_line(color = "#1d3557", linewidth = 0.5) +
  labs(x = "Date", y = "Simple return", title = paste("Daily returns for", asset_a))

TODO: Explain what the mean, standard deviation, skewness, and kurtosis imply for risk management.

1(c)(i) Beginning Value Of A GBP 1 Million Portfolio

asset_a_value_path <- portfolio_value_path(asset_a_returns, end_value_millions = 1)

beginning_value_millions <- asset_a_value_path[1]
beginning_value_currency <- beginning_value_millions * 1000000

data.frame(
  beginning_value_millions = round(beginning_value_millions, 6),
  beginning_value_currency = round(beginning_value_currency, 2)
) %>%
  kable(caption = "Implied beginning value of the Asset A portfolio")
Implied beginning value of the Asset A portfolio
beginning_value_millions beginning_value_currency
0.433362 433362.3

1(c)(ii) VaR And ES For 18 February 2026

asset_a_risk_table <- method_comparison_table(
  returns = asset_a_returns,
  portfolio_value_millions = 1,
  p = 0.95
)

kable(asset_a_risk_table %>% mutate(across(c(VaR, ES), round, 6)),
  caption = "One-day 95% VaR and ES for Asset A, expressed in millions"
)
One-day 95% VaR and ES for Asset A, expressed in millions
Method VaR ES
Historical Simulation 0.018023 0.031139
Age-weighted Historical Simulation 0.015449 0.022904
Hull-White 0.014988 0.021261
Parametric Normal 0.020735 0.026161
Parametric Normal with Volatility Adjustment 0.013739 0.017341

TODO: Check whether your course expects a different parametric distribution than Normal. If yes, replace the parametric helper accordingly.

1(c)(iii) Variation Across Approaches

TODO: Compare the five methods for the same risk measure and explain why they differ.

1(c)(iv) Variation Between VaR And ES

TODO: Compare VaR and ES within each method and explain the practical meaning.

2. Two-Asset VaR Using Hull-White

asset_b_returns <- returns[, asset_b]

# Equal-weight portfolio because £1m is invested in A and £1m in B
portfolio_ab_returns <- 0.5 * asset_a_returns + 0.5 * asset_b_returns

risk_a_hw <- hull_white_var_es(asset_a_returns, portfolio_value_millions = 1, p = 0.95)
risk_b_hw <- hull_white_var_es(asset_b_returns, portfolio_value_millions = 1, p = 0.95)
risk_ab_hw <- hull_white_var_es(portfolio_ab_returns, portfolio_value_millions = 2, p = 0.95)

question_2_table <- data.frame(
  portfolio = c("Asset A only", "Asset B only", "A and B combined"),
  VaR_millions = c(risk_a_hw$VaR, risk_b_hw$VaR, risk_ab_hw$VaR)
)

kable(
  question_2_table %>% mutate(across(where(is.numeric), ~ round(.x, 6))),
  caption = "Hull-White 95% VaR results for Question 2"
)
Hull-White 95% VaR results for Question 2
portfolio VaR_millions
Asset A only 0.014988
Asset B only 0.050399
A and B combined 0.057150
comparison <- data.frame(
  lhs = risk_a_hw$VaR + risk_b_hw$VaR,
  rhs = risk_ab_hw$VaR,
  lhs_less_than_rhs = (risk_a_hw$VaR + risk_b_hw$VaR) < risk_ab_hw$VaR
)

kable(
  comparison %>% mutate(across(where(is.numeric), ~ round(.x, 6))),
  caption = "Check of whether VaR(A) + VaR(B) < VaR(A+B)"
)
Check of whether VaR(A) + VaR(B) < VaR(A+B)
lhs rhs lhs_less_than_rhs
0.065387 0.05715 FALSE

TODO: Briefly comment on whether diversification reduces measured risk in your data.

3. Portfolio Composition Decision

3(a) Minimum-Risk Portfolio With A And B Or A And C

best_with_b <- two_asset_hull_white_search(
  returns_xts = returns,
  asset_a = asset_a,
  asset_other = asset_b,
  min_weight_a = 0.2,
  p = 0.95,
  portfolio_value_millions = 2
)

best_with_c <- two_asset_hull_white_search(
  returns_xts = returns,
  asset_a = asset_a,
  asset_other = asset_c,
  min_weight_a = 0.2,
  p = 0.95,
  portfolio_value_millions = 2
)

comparison_3a <- bind_rows(
  cbind(pair = paste(asset_a, "+", asset_b), best_with_b),
  cbind(pair = paste(asset_a, "+", asset_c), best_with_c)
)

kable(
  comparison_3a %>% mutate(across(where(is.numeric), ~ round(.x, 6))),
  caption = "Best two-asset portfolios subject to the 20% Asset A constraint"
)
Best two-asset portfolios subject to the 20% Asset A constraint
pair weight_a weight_other VaR ES expected_daily_profit
SPY + GLD 1.00 0.00 0.029976 0.042522 0.001247
SPY + TLT 0.35 0.65 0.015983 0.021381 0.000301

TODO: State which pairing produces the lower 95% Hull-White ES and explain why.

3(b) Expected Profit Versus The Higher-Return Single Asset

asset_means <- data.frame(
  asset = colnames(returns),
  mean_daily_return = colMeans(returns)
)

best_single_asset <- asset_means %>%
  arrange(desc(mean_daily_return)) %>%
  slice(1) %>%
  mutate(expected_daily_profit_millions = 2 * mean_daily_return)

kable(
  asset_means %>% mutate(across(where(is.numeric), ~ round(.x, 6))),
  caption = "Average simple daily returns by asset"
)
Average simple daily returns by asset
asset mean_daily_return
SPY SPY 0.000624
GLD GLD 0.000821
TLT TLT -0.000104
kable(
  best_single_asset %>% mutate(across(where(is.numeric), ~ round(.x, 6))),
  caption = "Higher-return single-asset alternative for GBP 2 million"
)
Higher-return single-asset alternative for GBP 2 million
asset mean_daily_return expected_daily_profit_millions
GLD GLD 0.000821 0.001641

TODO: Compare the expected profit of your minimum-risk portfolio against the all-in higher-return alternative, and comment on whether the trade-off feels worthwhile.

Final Checks