2026The objective of this project is to leverage Artificial Intelligence (AI) tools to assist in investment decision-making. Students will move from strategy formulation and asset selection to quantitative backtesting, ultimately validating whether the proposed strategy can generate abnormal returns (Alpha).
TODO: Detail the logic behind your portfolio construction. What is your specific source of Alpha?
TODO: Document your interaction with AI. What specific prompts were used? How did the AI assist in narrowing down the universe of stocks or optimizing weights?
# Define Assets and Benchmark
tickers <- c('AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA',
'META', 'NVDA', 'JPM', 'V', 'UNH')
benchmark_ticker <- 'SPY'
# Display as a table (weights will be filled in after optimization)
asset_table <- data.frame(
Ticker = tickers,
Company = c("Apple", "Microsoft", "Alphabet", "Amazon", "Tesla",
"Meta", "NVIDIA", "JPMorgan Chase", "Visa", "UnitedHealth"),
Initial_Weight = rep(NA_real_, length(tickers)) # updated after optimization
)
knitr::kable(asset_table, caption = "Selected Portfolio Assets (max 10)")| Ticker | Company | Initial_Weight |
|---|---|---|
| AAPL | Apple | NA |
| MSFT | Microsoft | NA |
| GOOGL | Alphabet | NA |
| AMZN | Amazon | NA |
| TSLA | Tesla | NA |
| META | Meta | NA |
| NVDA | NVIDIA | NA |
| JPM | JPMorgan Chase | NA |
| V | Visa | NA |
| UNH | UnitedHealth | NA |
The benchmark is SPY (SPDR S&P 500 ETF Trust), which tracks the S&P 500 index.
TODO: Justify why this benchmark is a relevant comparison for your strategy.
# Install if needed:
# install.packages(c("tidyquant", "PortfolioAnalytics",
# "ROI", "ROI.plugin.glpk", "ROI.plugin.quadprog"))
library(tidyquant)
library(PortfolioAnalytics)
library(ROI)
library(ROI.plugin.glpk)
library(ROI.plugin.quadprog)
library(PerformanceAnalytics)
library(tidyr)
library(xts)start_date <- Sys.Date() - lubridate::years(3)
# Portfolio prices
prices <- tq_get(tickers,
from = start_date,
get = "stock.prices")
# Daily returns – wide xts format
returns_wide <- prices |>
dplyr::group_by(symbol) |>
tq_transmute(select = adjusted,
mutate_fun = periodReturn,
period = "daily",
col_rename = "Ra") |>
tidyr::pivot_wider(names_from = symbol,
values_from = Ra)
returns_multi <- xts(returns_wide[, -1], order.by = returns_wide$date)
head(returns_multi)## AAPL MSFT GOOGL AMZN TSLA
## 2023-05-26 0.0000000000 0.000000000 0.000000000 0.000000000 0.00000000
## 2023-05-30 0.0106595237 -0.005046638 -0.007543615 0.012904863 0.04136256
## 2023-05-31 -0.0002819645 -0.008514343 -0.006468639 -0.008877214 0.01377008
## 2023-06-01 0.0160224769 0.012759281 0.006917759 0.018162173 0.01760414
## 2023-06-02 0.0047754980 0.008479083 0.007678711 0.012055090 0.03108133
## 2023-06-05 -0.0075712360 0.001610044 0.010748329 0.008450729 0.01701173
## META NVDA JPM V UNH
## 2023-05-26 0.000000000 0.000000000 0.000000000 0.000000000 0.000000000
## 2023-05-30 0.001831879 0.029913451 0.003797185 -0.014977069 -0.003468282
## 2023-05-31 0.008380280 -0.056767570 -0.012730599 -0.002752022 0.015400876
## 2023-06-01 0.029805024 0.051171020 0.013779143 0.024747623 0.013114760
## 2023-06-02 0.000000000 -0.011139042 0.021006140 0.010110429 0.012053367
## 2023-06-05 -0.004475180 -0.003966906 -0.009824180 -0.008829134 -0.002782263
# Define portfolio specification
port_spec <- portfolio.spec(assets = tickers) |>
add.constraint(type = "full_investment") |>
add.constraint(type = "long_only") |>
add.objective(type = "return", name = "mean") |>
add.objective(type = "risk", name = "StdDev")
# Run optimization
opt_weights <- optimize.portfolio(
R = returns_multi,
portfolio = port_spec,
optimize_method = "ROI",
max_sharpe = TRUE
)
optimized_w <- extractWeights(opt_weights)
print(round(optimized_w, 4))## AAPL MSFT GOOGL AMZN TSLA META NVDA JPM V UNH
## 0.0000 0.0000 0.1757 0.0000 0.0000 0.0000 0.8243 0.0000 0.0000 0.0000
# Update asset table with optimized weights
asset_table$Initial_Weight <- round(optimized_w[tickers], 4)
knitr::kable(asset_table, caption = "Optimized Portfolio Weights")| Ticker | Company | Initial_Weight |
|---|---|---|
| AAPL | Apple | 0.0000 |
| MSFT | Microsoft | 0.0000 |
| GOOGL | Alphabet | 0.1757 |
| AMZN | Amazon | 0.0000 |
| TSLA | Tesla | 0.0000 |
| META | Meta | 0.0000 |
| NVDA | NVIDIA | 0.8243 |
| JPM | JPMorgan Chase | 0.0000 |
| V | Visa | 0.0000 |
| UNH | UnitedHealth | 0.0000 |
# Portfolio returns with optimized weights
portfolio_returns <- Return.portfolio(returns_multi,
weights = optimized_w)
colnames(portfolio_returns) <- "AI_Portfolio"
# Benchmark returns
bmark_raw <- tq_get(benchmark_ticker, from = start_date) |>
tq_transmute(select = adjusted,
mutate_fun = periodReturn,
period = "daily",
col_rename = "Benchmark")
benchmark_returns <- xts(bmark_raw[, "Benchmark"], order.by = bmark_raw$date)
# Merge
combined_returns <- merge.xts(portfolio_returns, benchmark_returns)charts.PerformanceSummary(
combined_returns,
main = "Portfolio vs Benchmark Performance",
colorset = c("#2196F3", "#FF5722"),
legend.loc = "topleft"
)# Annualised returns, Sharpe, volatility
ann_table <- table.AnnualizedReturns(combined_returns, scale = 252, Rf = 0)
knitr::kable(round(ann_table, 4), caption = "Annualised Performance Metrics")| AI_Portfolio | Benchmark | |
|---|---|---|
| Annualized Return | 0.7295 | 0.2285 |
| Annualized Std Dev | 0.4342 | 0.1513 |
| Annualized Sharpe (Rf=0%) | 1.6799 | 1.5098 |
# Maximum Drawdown
mdd_table <- table.DrawdownsRatio(combined_returns)
knitr::kable(round(mdd_table, 4), caption = "Drawdown Statistics")| AI_Portfolio | Benchmark | |
|---|---|---|
| Sterling ratio | 1.5859 | 0.7945 |
| Calmar ratio | 2.0265 | 1.2181 |
| Burke ratio | 1.1190 | 0.9446 |
| Pain index | 0.0648 | 0.0197 |
| Ulcer index | 0.0942 | 0.0347 |
| Pain ratio | 11.2506 | 11.5960 |
| Martin ratio | 7.7448 | 6.5757 |
# Alpha & Beta vs benchmark
capm_table <- table.CAPM(portfolio_returns, benchmark_returns, scale = 252, Rf = 0)
knitr::kable(round(capm_table, 4), caption = "CAPM: Alpha & Beta")| AI_Portfolio to Benchmark | |
|---|---|
| Alpha | 0.0009 |
| Beta | 1.9657 |
| Alpha Robust | 0.0007 |
| Beta Robust | 1.8112 |
| Beta+ | 1.9126 |
| Beta- | 1.8265 |
| Beta+ Robust | 1.7974 |
| Beta- Robust | 1.6778 |
| R-squared | 0.4692 |
| R-squared Robust | 0.4187 |
| Annualized Alpha | 0.2402 |
| Correlation | 0.6850 |
| Correlation p-value | 0.0000 |
| Tracking Error | 0.3485 |
| Active Premium | 0.5010 |
| Information Ratio | 1.4377 |
| Treynor Ratio | 0.3711 |
TODO: What insights did the AI provide that might have been overlooked by traditional analysis?
TODO: Do the backtesting results align with your initial hypothesis? If there is a discrepancy, what are the potential causes?
“Act as a Quantitative Researcher. I want to build a portfolio of 10 US-listed ETFs based on a ‘Quality Momentum’ factor. Explain the economic rationale for why high-quality companies with positive price momentum should deliver abnormal returns over the next 12 months.”
AI Response Summary: (paste or summarise the AI’s response here)
“I have a list of 50 technology stocks. Using the most recent quarterly earnings data, identify which 10 stocks have the highest Free Cash Flow yield and a Debt-to-Equity ratio below 0.5. Provide a summary of the AI’s reasoning for each pick.”
AI Response Summary: (paste or summarise the AI’s response here)
“Write a Python script using the
cvxpyorPyPortfolioOptlibrary to calculate the ‘Maximum Sharpe Ratio’ weights for a list of 10 tickers. Use the last 3 years of daily returns and apply a constraint where no single asset exceeds 20% of the total weight.”
AI Response Summary: (paste or summarise the AI’s response here)
| Criterion | Weight | Description |
|---|---|---|
| Logical Rigor | 30% | Is the strategy grounded in sound financial theory? |
| AI Integration | 30% | Does the student demonstrate effective collaboration with AI rather than passive reliance? |
| Data Accuracy | 20% | Are the backtesting charts clear and the calculations correct? |
| Presentation | 20% | How effectively does the student communicate the strategy’s potential? |
Rendered with R R version 4.5.1 (2025-06-13 ucrt) on 2026-05-26.