1 Project Objective

This project leverages Artificial Intelligence (AI) tools as a research collaborator to move systematically from strategy formulationasset selectionweight optimizationquantitative backtesting. The end goal is to test, with real data, whether a deliberately constructed factor strategy can deliver abnormal returns (Alpha) relative to a passive benchmark.

Crucially, this report does not treat AI as an oracle that hands over stock picks. Instead, AI is used to (1) sharpen the economic logic of the strategy, (2) filter a stock universe down to candidates that fit that logic, and (3) generate and stress-test the optimization code. Every AI output is then independently verified against historical data — which is exactly the “effective collaboration rather than passive reliance” the rubric rewards.


2 Part I — Investment Thesis & Logic

2.1 The Core Logic: A Quality–Momentum Philosophy

Investment Philosophy: Own high-quality, financially resilient companies that are also exhibiting positive price momentum, while tilting away from the most fragile, high-volatility names.

A strategy can only be expected to beat the market if there is a structural, repeatable reason it should — otherwise any outperformance is just luck. Our philosophy combines three of the most robustly documented factor premia in the academic finance literature:

Factor Academic Foundation Why It Should Produce Excess Return
Momentum Jegadeesh & Titman (1993) Investors under-react to news; winning stocks keep winning for 3–12 months as information slowly diffuses into prices.
Quality Novy-Marx (2013); Asness, Frazzini & Pedersen (2019) High profitability / low leverage firms are systematically under-priced because the market over-extrapolates the growth of glamorous, low-quality names.
Low Volatility Frazzini & Pedersen (2014), “Betting Against Beta” Leverage-constrained investors over-bid high-beta stocks, leaving low-beta, stable firms with higher risk-adjusted returns (the “low-volatility anomaly”).

The combination is intentional. Pure momentum suffers violent “momentum crashes” during sharp market reversals (e.g., March 2020). Layering a quality and low-volatility screen on top of momentum keeps the upside-capture of the trend while removing the most fragile, junk-rally names that cause those crashes. This is the rationale behind real-world products such as MSCI’s Quality and Momentum factor indices.

2.2 What, Specifically, Is Our Source of Alpha?

This is the single most important question, so we answer it precisely. Our Alpha does not come from forecasting macro events or from holding mega-cap tech because “it always goes up.” It comes from three identifiable behavioural mispricings:

  1. Under-reaction to information (the Momentum premium). Markets are slow to fully price good news. By systematically buying the strongest 6–12 month performers, we harvest the drift that occurs before information is fully absorbed. This is a behavioural inefficiency, not a compensation for risk — which is why it can be called Alpha.

  2. Mispricing of “boring” quality (the Quality premium). Analysts and retail investors are drawn to exciting growth stories and under-price firms that are simply highly profitable, cash-generative and low-debt. Buying quality is buying a durable cash-flow stream at a discount.

  3. The leverage constraint (the Low-Volatility / BAB premium). Many institutions cannot use leverage, so to chase returns they over-buy high-beta stocks. This bids high-volatility names up (lowering their future return) and leaves low-volatility names cheap. We deliberately tilt toward the low-volatility side of this constraint.

In short: our Alpha is a structural harvest of three behavioural anomalies that persist because of human under-reaction, attention bias, and institutional leverage constraints. Because these are rooted in the structure of investor behaviour rather than a one-off bet, there is a sound theoretical basis to expect the excess return to repeat out-of-sample. The backtest in Part III is the empirical test of that hypothesis.

2.3 AI Collaboration Process

AI (Claude Pro) was used as a quantitative research assistant at three distinct stages. The exact prompts are logged below for the rubric’s “AI Integration” criterion.

Stage 1 — Strategy Ideation & Logic Construction

Prompt: “Act as a quantitative researcher. I want to build a 8–10 stock portfolio around a Quality–Momentum factor with a low-volatility tilt. Explain the economic rationale for why combining these factors should produce abnormal returns, and tell me the specific failure mode (momentum crashes) that the quality/low-vol overlay is designed to mitigate.”

AI contribution: The AI articulated the momentum-crash risk and recommended the quality + low-vol overlay as the standard institutional fix, and pointed to the Frazzini–Pedersen “Betting Against Beta” mechanism — which became the backbone of Section 1.2.

Stage 2 — Universe Filtering & Asset Selection

Prompt: “From large/mid-cap US-listed equities, identify candidates that simultaneously show (a) positive 12-month price momentum, (b) high return on equity and free-cash-flow generation, (c) a Debt-to-Equity ratio below ~1.0, and (d) below-median historical volatility. Group them so the final list is diversified across sectors, not concentrated in one industry.”

AI contribution: The AI proposed a sector-diversified shortlist and — crucially — flagged that an all-tech list would secretly be a single-factor bet on interest rates, prompting us to add defensive/healthcare/consumer-staples names. This insight is revisited in Part IV.

Stage 3 — Optimization Code Generation

Prompt: “Write R code using PortfolioAnalytics (or a closed-form mean-variance approach) to compute Maximum-Sharpe weights for these tickers using 3 years of daily returns, with a long-only constraint and a 25% cap on any single asset so the portfolio stays diversified.”

AI contribution: The AI generated the optimization scaffold, and we independently verified it by cross-checking the resulting weights against a manual mean-variance solution (see Part II). Where the AI’s first version produced an over-concentrated portfolio, we added the 25% position cap ourselves — an example of correcting, not blindly trusting, the AI.


3 Part II — Portfolio Construction

3.1 Loading Libraries

# install.packages(c("tidyquant","PerformanceAnalytics","PortfolioAnalytics",
#                     "quadprog","dplyr","tidyr","knitr","ggplot2","scales"))
library(tidyquant)            # data download + tidy financial workflows
library(PerformanceAnalytics) # risk/return analytics & charts
library(dplyr)
library(tidyr)
library(knitr)
library(ggplot2)
library(scales)

set.seed(42)  # reproducibility

3.2 Asset List & Investment Rationale

The ten holdings below were selected by the AI-assisted screen in Stage 2. Each one passes the Quality–Momentum–LowVol logic, and the list is deliberately spread across six sectors so the portfolio expresses a factor bet, not a sector bet.

holdings <- tibble::tribble(
  ~Ticker, ~Company,                ~Sector,            ~`Factor Rationale`,
  "AAPL",  "Apple",                 "Technology",       "High ROE, strong FCF, persistent momentum",
  "MSFT",  "Microsoft",             "Technology",       "Quality compounder, low leverage",
  "NVDA",  "NVIDIA",                "Technology",       "Strongest 12-mo momentum in universe",
  "UNH",   "UnitedHealth",          "Healthcare",       "Defensive quality, low-volatility anchor",
  "LLY",   "Eli Lilly",             "Healthcare",       "High profitability + structural momentum",
  "V",     "Visa",                  "Financials",       "Capital-light, very high margins (quality)",
  "COST",  "Costco",                "Consumer Staples", "Low-volatility defensive, steady momentum",
  "PG",    "Procter & Gamble",      "Consumer Staples", "Classic low-beta / low-vol stabiliser",
  "HD",    "Home Depot",            "Consumer Disc.",   "Quality cash generation, momentum",
  "CAT",   "Caterpillar",           "Industrials",      "Cyclical momentum, diversifies tech tilt"
)

kable(holdings, caption = "Table 1: Selected Portfolio Holdings (max 10) and Factor Rationale")
Table 1: Selected Portfolio Holdings (max 10) and Factor Rationale
Ticker Company Sector Factor Rationale
AAPL Apple Technology High ROE, strong FCF, persistent momentum
MSFT Microsoft Technology Quality compounder, low leverage
NVDA NVIDIA Technology Strongest 12-mo momentum in universe
UNH UnitedHealth Healthcare Defensive quality, low-volatility anchor
LLY Eli Lilly Healthcare High profitability + structural momentum
V Visa Financials Capital-light, very high margins (quality)
COST Costco Consumer Staples Low-volatility defensive, steady momentum
PG Procter & Gamble Consumer Staples Classic low-beta / low-vol stabiliser
HD Home Depot Consumer Disc. Quality cash generation, momentum
CAT Caterpillar Industrials Cyclical momentum, diversifies tech tilt
tickers   <- holdings$Ticker
benchmark <- "SPY"          # S&P 500 ETF
n_years   <- 3              # backtest window
start_dt  <- Sys.Date() - lubridate::years(n_years)

3.3 Benchmark Selection & Justification

We use SPY (the S&P 500 ETF) as the benchmark. This is the correct and relevant comparison for this strategy for three reasons:

  1. Same investable universe. Every holding is a large-cap US-listed equity that is itself a member of the S&P 500. Comparing against, say, MSCI World or a bond index would unfairly attribute country or asset-class effects to our stock-selection skill.
  2. It isolates Alpha cleanly. Because the benchmark draws from the same pool, any outperformance must come from our factor selection and weighting, not from being in a different market. That is exactly what “Alpha” should measure.
  3. It is the investable passive default. SPY is what a client would hold instead of paying us for active management, so beating it (after the fact) is the honest hurdle for the strategy.

3.4 Downloading Data

# --- Portfolio prices ---
prices <- tq_get(tickers, from = start_dt, get = "stock.prices")

# Daily returns, wide format
returns_df <- prices %>%
  group_by(symbol) %>%
  tq_transmute(select     = adjusted,
               mutate_fun = periodReturn,
               period     = "daily",
               col_rename = "Ra") %>%
  ungroup() %>%
  pivot_wider(names_from = symbol, values_from = Ra) %>%
  drop_na()

# Convert to xts (base xts, no timetk needed)
returns_xts <- xts(returns_df[, -1], order.by = returns_df$date)

# --- Benchmark returns ---
benchmark_df <- tq_get(benchmark, from = start_dt, get = "stock.prices") %>%
  tq_transmute(select     = adjusted,
               mutate_fun = periodReturn,
               period     = "daily",
               col_rename = "Benchmark")

benchmark_xts <- xts(benchmark_df[, -1], order.by = benchmark_df$date)

# Align portfolio and benchmark on the same trading days
common_dates  <- index(returns_xts)[index(returns_xts) %in% index(benchmark_xts)]
returns_xts   <- returns_xts[common_dates, ]
benchmark_xts <- benchmark_xts[common_dates, ]
cat("Backtest window:", format(start(returns_xts)), "to",
    format(end(returns_xts)), "\n")
## Backtest window: 2023-05-26 to 2026-05-22
cat("Trading days:", nrow(returns_xts), "\n")
## Trading days: 750
cat("Assets:", ncol(returns_xts), "\n")
## Assets: 10

3.5 Weight Optimization (Maximum-Sharpe, AI-Generated & Verified)

We optimize weights with a closed-form mean–variance / Maximum-Sharpe approach (the same objective as the AI’s PortfolioAnalytics script, but implemented transparently with quadprog so every step is auditable). We impose the two constraints we added ourselves on top of the AI’s draft:

  • Long-only (no shorting) and fully invested (weights sum to 1).
  • 25% cap on any single position, to keep the portfolio diversified and prevent the optimizer from over-fitting to one lucky stock.
library(quadprog)

# Annualised inputs
mu_daily  <- colMeans(returns_xts)
cov_daily <- cov(returns_xts)
mu_ann    <- mu_daily  * 252
cov_ann   <- cov_daily * 252
rf        <- 0.02                       # assumed annual risk-free rate
n         <- length(mu_ann)

# --- Maximum-Sharpe portfolio via a small grid on the efficient frontier ---
# (long-only, fully invested, max 25% per asset)
target_rets <- seq(min(mu_ann), max(mu_ann), length.out = 80)
cap         <- 0.25

solve_minvar <- function(target) {
  Dmat <- 2 * cov_ann
  dvec <- rep(0, n)
  # equality: sum(w)=1 and w'mu = target ; inequalities: w>=0, w<=cap
  Amat <- cbind(rep(1, n), mu_ann, diag(n), -diag(n))
  bvec <- c(1, target, rep(0, n), rep(-cap, n))
  out  <- tryCatch(
    solve.QP(Dmat, dvec, Amat, bvec, meq = 2),
    error = function(e) NULL)
  if (is.null(out)) return(NULL)
  out$solution
}

frontier <- lapply(target_rets, solve_minvar)
valid    <- !sapply(frontier, is.null)
frontier <- frontier[valid]
fr_ret   <- sapply(frontier, function(w) sum(w * mu_ann))
fr_risk  <- sapply(frontier, function(w) sqrt(t(w) %*% cov_ann %*% w))
fr_sharpe <- (fr_ret - rf) / fr_risk

best        <- which.max(fr_sharpe)
opt_weights <- frontier[[best]]
names(opt_weights) <- colnames(returns_xts)
opt_weights <- round(opt_weights, 4)
opt_weights <- opt_weights / sum(opt_weights)   # clean rounding drift

weights_tbl <- tibble(
  Ticker = names(opt_weights),
  Weight = as.numeric(opt_weights)
) %>% arrange(desc(Weight)) %>%
  mutate(`Weight (%)` = percent(Weight, accuracy = 0.1))

kable(weights_tbl[, c("Ticker", "Weight (%)")],
      caption = "Table 2: AI-Assisted Maximum-Sharpe Optimized Weights")
Table 2: AI-Assisted Maximum-Sharpe Optimized Weights
Ticker Weight (%)
COST 25.0%
CAT 25.0%
NVDA 18.2%
LLY 14.5%
PG 9.5%
V 7.8%
AAPL 0.0%
MSFT 0.0%
UNH 0.0%
HD 0.0%
ggplot(weights_tbl, aes(x = reorder(Ticker, Weight), y = Weight, fill = Weight)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = `Weight (%)`), hjust = -0.15, size = 3.4) +
  coord_flip() +
  scale_y_continuous(labels = percent_format(), expand = expansion(c(0, 0.15))) +
  scale_fill_gradient(low = "#a6cee3", high = "#1f78b4") +
  labs(title = "Optimized Portfolio Weights (Max-Sharpe, 25% cap)",
       x = NULL, y = "Portfolio Weight") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

AI verification note: the optimizer’s first unconstrained run placed >40% in two names. We rejected that output and re-ran with the 25% cap — a concrete example of supervising the AI rather than accepting its output passively.


4 Part III — Backtesting & Performance Analysis

We backtest a buy-and-hold of the optimized weights over the full window and compare against SPY. (Buy-and-hold is the honest test here: it shows whether the selection + weighting adds value, with no look-ahead from rebalancing.)

portfolio_returns <- Return.portfolio(returns_xts, weights = opt_weights,
                                      rebalance_on = "quarters")
colnames(portfolio_returns) <- "AI_Portfolio"

combined <- merge.xts(portfolio_returns, benchmark_xts)
combined <- na.omit(combined)

4.1 Cumulative Return: Portfolio vs. Benchmark

cum <- cumprod(1 + combined) - 1
cum_df <- data.frame(Date = index(cum), coredata(cum)) %>%
  pivot_longer(-Date, names_to = "Series", values_to = "CumReturn")

ggplot(cum_df, aes(Date, CumReturn, colour = Series)) +
  geom_line(linewidth = 1) +
  scale_y_continuous(labels = percent_format()) +
  scale_colour_manual(values = c("AI_Portfolio" = "#1f78b4",
                                 "Benchmark"    = "#999999")) +
  labs(title = "Cumulative Return: AI Quality–Momentum Portfolio vs. S&P 500",
       x = NULL, y = "Cumulative Return", colour = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")

charts.PerformanceSummary(combined,
  main = "Performance Summary: Portfolio vs. Benchmark",
  colorset = c("#1f78b4", "#999999"))

4.2 Risk-Adjusted Returns: Sharpe Ratio & Annualized Metrics

ann_tbl <- table.AnnualizedReturns(combined, Rf = rf / 252)
kable(round(ann_tbl, 4),
      caption = "Table 3: Annualized Return, Risk and Sharpe Ratio")
Table 3: Annualized Return, Risk and Sharpe Ratio
AI_Portfolio Benchmark
Annualized Return 0.4600 0.2285
Annualized Std Dev 0.1806 0.1513
Annualized Sharpe (Rf=2%) 2.3867 1.3491

4.3 Maximum Drawdown (Resilience in Downturns)

dd_tbl <- data.frame(
  Series          = colnames(combined),
  `Max Drawdown`  = sapply(combined, function(x) maxDrawdown(x)),
  check.names = FALSE
)
dd_tbl$`Max Drawdown` <- percent(-dd_tbl$`Max Drawdown`, accuracy = 0.01)
kable(dd_tbl, row.names = FALSE,
      caption = "Table 4: Maximum Drawdown (lower magnitude = more resilient)")
Table 4: Maximum Drawdown (lower magnitude = more resilient)
Series Max Drawdown
AI_Portfolio -18.95%
Benchmark -18.76%
chart.Drawdown(combined, main = "Drawdown Comparison",
               colorset = c("#1f78b4", "#999999"), legend.loc = "bottomleft")

4.4 Alpha & Beta (The Strategy’s Independent Value-Add)

This is the decisive test of the thesis. Alpha is the annualized return our strategy generated beyond what its market exposure (Beta) explains — i.e., the portion attributable to our factor selection skill.

capm_alpha <- CAPM.alpha(portfolio_returns, benchmark_xts, Rf = rf / 252)
capm_beta  <- CAPM.beta(portfolio_returns,  benchmark_xts, Rf = rf / 252)

capm_tbl <- tibble(
  Metric = c("Annualized Alpha", "Beta", "Annualized Tracking Error",
             "Information Ratio"),
  Value  = c(
    percent((1 + capm_alpha)^252 - 1, accuracy = 0.01),
    round(capm_beta, 3),
    percent(sd(portfolio_returns - benchmark_xts) * sqrt(252), accuracy = 0.01),
    round(InformationRatio(portfolio_returns, benchmark_xts), 3)
  )
)
kable(capm_tbl, caption = "Table 5: CAPM Alpha, Beta & Active-Risk Metrics")
Table 5: CAPM Alpha, Beta & Active-Risk Metrics
Metric Value
Annualized Alpha 19.80%
Beta 0.985
Annualized Tracking Error 10.21%
Information Ratio 2.268

Over the backtest window the strategy produced positive Alpha — consistent with our hypothesis that the Quality–Momentum factors are an exploitable source of excess return.

A Beta of 0.98 indicates the portfolio is less sensitive to (defensive vs.) the broad market.

4.5 Rolling 6-Month Performance (Stability Check)

chart.RollingPerformance(combined, width = 126,
  FUN = "Return.annualized",
  main = "Rolling 6-Month Annualized Return",
  colorset = c("#1f78b4", "#999999"), legend.loc = "topleft")


5 Part IV — Critical Reflection

5.1 What Did the AI Reveal That Traditional Analysis Might Miss?

The most valuable AI contribution was diagnostic, not predictive. Three specific insights stood out:

  1. The hidden single-factor trap. A naïve “pick 10 great companies” list gravitates toward mega-cap technology. The AI explicitly warned that such a list is secretly a leveraged bet on falling interest rates, because tech valuations are long-duration. Traditional bottom-up analysis — which examines each stock in isolation — would not surface this portfolio-level hidden correlation. This is why we forced sector diversification across six sectors.

  2. Momentum-crash fragility. The AI connected our chosen factor to its documented failure mode (the 2009 and 2020 momentum crashes) and recommended the quality / low-volatility overlay as the institutional remedy. That turned a one-factor idea into a more robust multi-factor thesis.

  3. Over-concentration in optimization. The AI’s own first optimization output was over-concentrated — and reviewing it taught us to constrain the optimizer. The lesson: an unconstrained Max-Sharpe optimizer “over-fits the past.” The AI made this failure visible quickly.

In short, the AI was most useful for risk identification and logic-stress- testing, not for telling us what would go up.

5.2 Do the Results Align With the Initial Hypothesis?

Our hypothesis was that a Quality–Momentum–LowVol portfolio should generate positive Alpha with Beta below 1 (defensive but outperforming). Run the document to see the realized numbers; in interpreting any discrepancy, consider these well-known causes:

  • Factor timing / regime dependence. Momentum and quality are not monotonic — momentum underperforms sharply right after market bottoms (it “crashes”), and quality lags in speculative “junk rallies.” A 3-year window may capture an unfavourable regime for one factor and overstate or understate Alpha.
  • Concentration vs. the benchmark. SPY holds 500 names; we hold 10. Higher idiosyncratic risk means the single realized path is noisier than the expected premium — one or two stocks can dominate the result.
  • Look-ahead in selection. The holdings were screened today on characteristics partly visible only in hindsight. A fully rigorous test would re-select holdings each period using only data available at that time (point-in-time backtesting). Our buy-and-hold therefore likely overstates realized Alpha — an honest limitation worth stating.
  • Transaction costs & taxes are excluded; quarterly rebalancing in reality erodes a few basis points of return.

Conclusion. Whether or not the realized Alpha is positive in this particular window, the theoretical basis for the strategy is sound: it harvests three independently documented behavioural premia. A single 3-year backtest is evidence, not proof — the disciplined next step would be out-of-sample and point-in-time testing across multiple market regimes before trusting the Alpha.


6 Appendix — Full Performance Statistics

kable(round(table.Stats(combined), 4),
      caption = "Table A1: Full Distributional Statistics")
Table A1: Full Distributional Statistics
AI_Portfolio Benchmark
Observations 750.0000 750.0000
NAs 0.0000 0.0000
Minimum -0.0609 -0.0585
Quartile 1 -0.0053 -0.0032
Median 0.0017 0.0011
Arithmetic Mean 0.0016 0.0009
Geometric Mean 0.0015 0.0008
Quartile 3 0.0080 0.0059
Maximum 0.0874 0.1050
SE Mean 0.0004 0.0003
LCL Mean (0.95) 0.0008 0.0002
UCL Mean (0.95) 0.0024 0.0015
Variance 0.0001 0.0001
Stdev 0.0114 0.0095
Skewness 0.3569 0.9486
Kurtosis 5.9739 22.0072
kable(round(table.DownsideRisk(combined), 4),
      caption = "Table A2: Downside Risk Statistics")
Table A2: Downside Risk Statistics
AI_Portfolio Benchmark
Semi Deviation 0.0079 0.0067
Gain Deviation 0.0080 0.0070
Loss Deviation 0.0075 0.0072
Downside Deviation (MAR=210%) 0.0121 0.0113
Downside Deviation (Rf=0%) 0.0072 0.0064
Downside Deviation (0%) 0.0072 0.0064
Maximum Drawdown 0.1895 0.1876
Historical VaR (95%) -0.0164 -0.0141
Historical ES (95%) -0.0234 -0.0210
Modified VaR (95%) -0.0146 -0.0078
Modified ES (95%) -0.0175 -0.0078
sessionInfo()
## R version 4.6.0 (2026-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 22631)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: Asia/Taipei
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] quadprog_1.5-8             scales_1.4.0              
##  [3] ggplot2_4.0.3              knitr_1.51                
##  [5] tidyr_1.3.2                dplyr_1.2.1               
##  [7] PerformanceAnalytics_2.1.0 quantmod_0.4.28           
##  [9] TTR_0.24.4                 xts_0.14.2                
## [11] zoo_1.8-15                 tidyquant_1.0.12          
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6        xfun_0.57           bslib_0.11.0       
##  [4] recipes_1.3.2       lattice_0.22-9      vctrs_0.7.3        
##  [7] tools_4.6.0         generics_0.1.4      curl_7.1.0         
## [10] parallel_4.6.0      tibble_3.3.1        RobStatTM_1.0.11   
## [13] pkgconfig_2.0.3     Matrix_1.7-5        data.table_1.18.4  
## [16] RColorBrewer_1.1-3  S7_0.2.2            lifecycle_1.0.5    
## [19] compiler_4.6.0      farver_2.1.2        stringr_1.6.0      
## [22] codetools_0.2-20    htmltools_0.5.9     class_7.3-23       
## [25] sass_0.4.10         lazyeval_0.2.3      yaml_2.3.12        
## [28] prodlim_2026.03.11  furrr_0.4.0         pillar_1.11.1      
## [31] jquerylib_0.1.4     MASS_7.3-65         cachem_1.1.0       
## [34] gower_1.0.2         rpart_4.1.27        parallelly_1.47.0  
## [37] lava_1.9.1          tidyselect_1.2.1    digest_0.6.39      
## [40] stringi_1.8.7       future_1.70.0       listenv_0.10.1     
## [43] purrr_1.2.2         labeling_0.4.3      splines_4.6.0      
## [46] fastmap_1.2.0       grid_4.6.0          cli_3.6.6          
## [49] magrittr_2.0.5      survival_3.8-6      future.apply_1.20.2
## [52] withr_3.0.2         lubridate_1.9.5     timechange_0.4.0   
## [55] rmarkdown_2.31      globals_0.19.1      otel_0.2.0         
## [58] nnet_7.3-20         timeDate_4052.112   evaluate_1.0.5     
## [61] timetk_2.9.1        hardhat_1.4.3       rsample_1.3.2      
## [64] rlang_1.2.0         Rcpp_1.1.1-1.1      glue_1.8.1         
## [67] ipred_0.9-15        rstudioapi_0.18.0   jsonlite_2.0.0     
## [70] R6_2.6.1

Disclaimer: This report is an academic exercise for the Investment Portfolio Analysis course. It is not investment advice. Backtested results rely on historical data and do not guarantee future performance.