## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----simulate----------------------------------------------------------------- library(ROOT) set.seed(123) n_assets <- 100 # Asset features volatility <- runif(n_assets, 0.05, 0.40) # annualised volatility beta <- runif(n_assets, 0.5, 1.8) # market beta sector <- sample(c("Tech", "Finance", "Energy", "Health"), n_assets, replace = TRUE) # Simulate returns: r_i = beta_i * r_market + epsilon_i market <- rnorm(1000, 0.0005, 0.01) returns_mat <- sapply(seq_len(n_assets), function(i) beta[i] * market + rnorm(1000, 0, volatility[i] / sqrt(252)) ) # Per-asset return variance (the objective proxy ROOT will minimize) vsq <- apply(returns_mat, 2, var) dat_portfolio <- data.frame( vsq = vsq, vol = volatility, beta = beta, sector = as.integer(factor(sector)) ) head(dat_portfolio) ## ----risk-plot, fig.width = 6, fig.height = 4, fig.alt = "Scatter plot of asset beta vs volatility"---- plot( dat_portfolio$beta, dat_portfolio$vol, xlab = "Market beta", ylab = "Annualised volatility", pch = 16, col = "#4E79A7AA", main = "Asset universe: volatility vs beta" ) ## ----fit, message = FALSE, warning = FALSE------------------------------------ portfolio_fit <- ROOT( data = dat_portfolio, num_trees = 20, top_k_trees = TRUE, k = 10, seed = 42 ) ## ----print-------------------------------------------------------------------- print(portfolio_fit) ## ----summary------------------------------------------------------------------ summary(portfolio_fit) ## ----plot, fig.width = 7, fig.height = 5, fig.alt = "Characterized tree for portfolio selection"---- plot(portfolio_fit) ## ----weights------------------------------------------------------------------ dat_portfolio$w_opt <- portfolio_fit$D_rash$w_opt # Summary statistics by inclusion decision included <- dat_portfolio[dat_portfolio$w_opt == 1, ] excluded <- dat_portfolio[dat_portfolio$w_opt == 0, ] cat("Included assets (w = 1):", nrow(included), "\n") cat(" Mean beta: ", round(mean(included$beta), 3), "\n") cat(" Mean volatility: ", round(mean(included$vol), 3), "\n\n") cat("Excluded assets (w = 0):", nrow(excluded), "\n") cat(" Mean beta: ", round(mean(excluded$beta), 3), "\n") cat(" Mean volatility: ", round(mean(excluded$vol), 3), "\n") ## ----weights-plot, fig.width = 6, fig.height = 4, fig.alt = "Asset universe with inclusion decisions highlighted"---- plot( dat_portfolio$beta, dat_portfolio$vol, xlab = "Market beta", ylab = "Annualised volatility", pch = ifelse(dat_portfolio$w_opt == 1, 16, 4), col = ifelse(dat_portfolio$w_opt == 1, "#4E79A7", "#E15759"), main = "Portfolio inclusion decisions" ) legend( "topleft", legend = c("w = 1 (included)", "w = 0 (excluded)"), pch = c(16, 4), col = c("#4E79A7", "#E15759"), bty = "n" ) ## ----custom-obj, message = FALSE, warning = FALSE----------------------------- iqr_objective <- function(D) { w <- D$w if (sum(w) == 0) return(Inf) # Weighted IQR: compute quantiles using the included assets only included_vsq <- D$vsq[w == 1] diff(quantile(included_vsq, probs = c(0.25, 0.75))) } portfolio_fit_iqr <- ROOT( data = dat_portfolio, global_objective_fn = iqr_objective, num_trees = 20, top_k_trees = TRUE, k = 10, seed = 112 )