0. Install & Load Required Packages
required_packages <- c(
"tidyquant", "PortfolioAnalytics", "PerformanceAnalytics",
"ROI", "ROI.plugin.quadprog", "ROI.plugin.glpk",
"tidyverse", "xts", "zoo", "lubridate"
)
for (pkg in required_packages) {
if (!requireNamespace(pkg, quietly = TRUE)) {
install.packages(pkg, repos = "https://cloud.r-project.org")
}
}
library(tidyquant)
library(PortfolioAnalytics)
library(PerformanceAnalytics)
library(ROI)
library(ROI.plugin.quadprog)
library(tidyverse)
library(xts)
library(lubridate)
1. Define Universe
tickers <- c("AAPL", "MSFT", "NVDA", "V", "UNH",
"COST", "MA", "LLY", "GOOGL", "BRK-B")
benchmark_ticker <- "SPY"
start_date <- Sys.Date() - years(3)
end_date <- Sys.Date()
cat("Downloading data from", format(start_date), "to", format(end_date), "\n")
## Downloading data from 2023-05-26 to 2026-05-26
2. Download Price Data
raw_prices <- tq_get(
tickers,
from = start_date,
to = end_date,
get = "stock.prices"
)
benchmark_prices <- tq_get(
benchmark_ticker,
from = start_date,
to = end_date,
get = "stock.prices"
)
3. Compute Daily Returns
Fix: Replaced tk_xts() (requires
timetk) with base xts::xts() — no extra
package needed.
# --- Portfolio returns (long → wide → xts) ---
returns_long <- raw_prices %>%
group_by(symbol) %>%
tq_transmute(
select = adjusted,
mutate_fun = periodReturn,
period = "daily",
col_rename = "return"
) %>%
ungroup()
returns_wide <- returns_long %>%
pivot_wider(names_from = symbol, values_from = return) %>%
drop_na() %>%
arrange(date)
# Convert to xts using base xts() — no timetk required
returns_xts <- xts(
returns_wide[ , -1], # drop the date column
order.by = as.Date(returns_wide$date)
)
# --- Benchmark returns ---
benchmark_wide <- benchmark_prices %>%
tq_transmute(
select = adjusted,
mutate_fun = periodReturn,
period = "daily",
col_rename = "SPY"
) %>%
arrange(date)
benchmark_returns <- xts(
benchmark_wide[ , "SPY", drop = FALSE],
order.by = as.Date(benchmark_wide$date)
)
cat("Data loaded:", nrow(returns_xts), "trading days,",
ncol(returns_xts), "assets\n")
## Data loaded: 750 trading days, 10 assets
4. Portfolio Optimization — Maximum Sharpe Ratio
Constraint: each weight in \[0.02,
0.20\] — no single asset exceeds 20%
port_spec <- portfolio.spec(assets = colnames(returns_xts))
port_spec <- add.constraint(port_spec, type = "full_investment")
port_spec <- add.constraint(port_spec, type = "long_only")
port_spec <- add.constraint(
port_spec,
type = "box",
min = 0.02,
max = 0.20
)
port_spec <- add.objective(port_spec, type = "return", name = "mean")
port_spec <- add.objective(port_spec, type = "risk", name = "StdDev")
set.seed(42)
opt_result <- optimize.portfolio(
R = returns_xts,
portfolio = port_spec,
optimize_method = "ROI",
maxSR = TRUE
)
optimized_weights <- extractWeights(opt_result)
weight_df <- data.frame(
Ticker = names(optimized_weights),
Weight_Pct = round(optimized_weights * 100, 2)
) %>% arrange(desc(Weight_Pct))
knitr::kable(
weight_df,
col.names = c("Ticker", "Weight (%)"),
caption = "Optimized Portfolio Weights — Maximum Sharpe Ratio"
)
Optimized Portfolio Weights — Maximum Sharpe Ratio
| COST |
COST |
20.0 |
| GOOGL |
GOOGL |
20.0 |
| BRK-B |
BRK-B |
20.0 |
| NVDA |
NVDA |
15.9 |
| LLY |
LLY |
14.1 |
| AAPL |
AAPL |
2.0 |
| MSFT |
MSFT |
2.0 |
| V |
V |
2.0 |
| UNH |
UNH |
2.0 |
| MA |
MA |
2.0 |
5. Backtest — Portfolio Returns
portfolio_returns <- Return.portfolio(
returns_xts,
weights = optimized_weights,
rebalance_on = "quarters"
)
colnames(portfolio_returns) <- "Quality_Momentum"
benchmark_aligned <- benchmark_returns[index(portfolio_returns)]
combined <- merge.xts(portfolio_returns, benchmark_aligned, join = "inner")
colnames(combined) <- c("Quality_Momentum", "SPY")
7. Visualizations
7.2 Rolling 12-Month Sharpe Ratio
chart.RollingPerformance(
combined,
FUN = "SharpeRatio.annualized",
width = 252,
Rf = risk_free_rate,
main = "Rolling 12-Month Sharpe Ratio",
colorset = c("#1f77b4", "#ff7f0e"),
legend.loc = "topleft"
)
7.3 Drawdown Chart
chart.Drawdown(
combined,
main = "Drawdown Comparison: Portfolio vs SPY",
colorset = c("#1f77b4", "#ff7f0e"),
legend.loc = "bottomleft"
)
7.4 Optimized Portfolio Weights
barplot(
sort(optimized_weights, decreasing = TRUE) * 100,
main = "Optimized Portfolio Weights (%)",
ylab = "Weight (%)",
xlab = "Ticker",
col = "#1f77b4",
las = 2,
ylim = c(0, 25)
)
abline(h = 20, col = "red", lty = 2, lwd = 1.5)
text(x = 1, y = 21.5, labels = "20% Max Constraint", col = "red", cex = 0.8)
8. Export Results to CSV
monthly_returns <- apply.monthly(combined, Return.cumulative)
write.csv(as.data.frame(monthly_returns), file = "monthly_returns.csv")
write.csv(weight_df, file = "optimized_weights.csv", row.names = FALSE)
cat("Files saved:\n")
## Files saved:
cat(" monthly_returns.csv\n")
## monthly_returns.csv
cat(" optimized_weights.csv\n")
## optimized_weights.csv
cat("\nBacktest complete!\n")
##
## Backtest complete!
Session Info
## R version 4.5.1 (2025-06-13)
## Platform: aarch64-apple-darwin20
## Running under: macOS Sonoma 14.3.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Asia/Taipei
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] lubridate_1.9.4 forcats_1.0.1
## [3] stringr_1.5.2 dplyr_1.1.4
## [5] purrr_1.1.0 readr_2.1.5
## [7] tidyr_1.3.1 tibble_3.3.0
## [9] ggplot2_4.0.2 tidyverse_2.0.0
## [11] ROI.plugin.quadprog_1.0-1 ROI_1.0-2
## [13] PortfolioAnalytics_2.1.2 foreach_1.5.2
## [15] PerformanceAnalytics_2.1.0 quantmod_0.4.28
## [17] TTR_0.24.4 xts_0.14.1
## [19] zoo_1.8-14 tidyquant_1.0.12
##
## loaded via a namespace (and not attached):
## [1] rlang_1.1.6 magrittr_2.0.4
## [3] ROI.plugin.glpk_1.0-0 furrr_0.3.1
## [5] compiler_4.5.1 vctrs_0.6.5
## [7] lhs_1.2.1 quadprog_1.5-8
## [9] tune_2.0.1 pkgconfig_2.0.3
## [11] fastmap_1.2.0 backports_1.5.0
## [13] rmarkdown_2.29 prodlim_2025.04.28
## [15] tzdb_0.5.0 xfun_0.53
## [17] cachem_1.1.0 jsonlite_2.0.0
## [19] recipes_1.3.1 parallel_4.5.1
## [21] R6_2.6.1 bslib_0.9.0
## [23] stringi_1.8.7 rsample_1.3.1
## [25] RColorBrewer_1.1-3 parallelly_1.45.1
## [27] rpart_4.1.24 jquerylib_0.1.4
## [29] numDeriv_2016.8-1.1 Rcpp_1.1.0
## [31] dials_1.4.2 iterators_1.0.14
## [33] knitr_1.50 future.apply_1.20.0
## [35] Matrix_1.7-3 splines_4.5.1
## [37] nnet_7.3-20 timechange_0.3.0
## [39] tidyselect_1.2.1 rstudioapi_0.17.1
## [41] yaml_2.3.10 timeDate_4051.111
## [43] codetools_0.2-20 curl_7.0.0
## [45] ROI.plugin.symphony_1.0-0 listenv_0.9.1
## [47] lattice_0.22-7 withr_3.0.2
## [49] S7_0.2.0 evaluate_1.0.5
## [51] timetk_2.9.1 future_1.67.0
## [53] survival_3.8-3 pillar_1.11.1
## [55] checkmate_2.3.3 generics_0.1.4
## [57] hms_1.1.3 scales_1.4.0
## [59] globals_0.18.0 class_7.3-23
## [61] glue_1.8.0 slam_0.1-55
## [63] mco_1.17 GenSA_1.1.15
## [65] lazyeval_0.2.2 tools_4.5.1
## [67] data.table_1.17.8 gower_1.0.2
## [69] registry_0.5-1 grid_4.5.1
## [71] yardstick_1.3.2 RobStatTM_1.0.11
## [73] ipred_0.9-15 cli_3.6.5
## [75] DiceDesign_1.10 workflows_1.3.0
## [77] parsnip_1.4.1 pso_1.0.4
## [79] lava_1.8.2 Rsymphony_0.1-33
## [81] gtable_0.3.6 GPfit_1.0-9
## [83] sass_0.4.10 digest_0.6.37
## [85] farver_2.1.2 htmltools_0.5.8.1
## [87] Rglpk_0.6-5.1 lifecycle_1.0.4
## [89] hardhat_1.4.2 MASS_7.3-65