Describe your portfolio construction logic here. What is your specific source of Alpha? (e.g., “We apply a Quality-Momentum strategy targeting US large-cap stocks with high Free Cash Flow yield and positive 12-month price momentum.”)
Document how AI was used:
- What prompts did you use?
- How did AI help narrow the stock universe or optimise weights?
tibble(
Ticker = tickers,
Name = c("Apple", "Microsoft", "Alphabet", "Amazon", "Tesla",
"Meta", "NVIDIA", "JPMorgan", "Visa", "UnitedHealth"),
Sector = c("Tech", "Tech", "Tech", "Cons. Disc.", "Cons. Disc.",
"Tech", "Tech", "Financials", "Financials", "Healthcare"),
Initial_Weight = paste0(round(100 / length(tickers), 1), "%")
) |> kable(caption = "Selected Portfolio Assets — Initial Equal Weights")| Ticker | Name | Sector | Initial_Weight |
|---|---|---|---|
| AAPL | Apple | Tech | 10% |
| MSFT | Microsoft | Tech | 10% |
| GOOGL | Alphabet | Tech | 10% |
| AMZN | Amazon | Cons. Disc. | 10% |
| TSLA | Tesla | Cons. Disc. | 10% |
| META | Meta | Tech | 10% |
| NVDA | NVIDIA | Tech | 10% |
| JPM | JPMorgan | Financials | 10% |
| V | Visa | Financials | 10% |
| UNH | UnitedHealth | Healthcare | 10% |
# Wide return matrix
returns_wide <- prices |>
group_by(symbol) |>
tq_transmute(select = adjusted, mutate_fun = periodReturn,
period = "daily", col_rename = "ret") |>
pivot_wider(names_from = symbol, values_from = ret) |>
drop_na()
ret_mat <- as.matrix(returns_wide[ , -1]) # numeric matrix only
dates <- returns_wide$date
# Benchmark returns
bench_ret <- benchmark_prices |>
tq_transmute(select = adjusted, mutate_fun = periodReturn,
period = "daily", col_rename = "Benchmark") |>
drop_na()# ── Max-Sharpe via efficient frontier line search ─────────────────────────────
# Strategy: sweep target returns across the feasible range, solve min-variance
# QP at each point (full-investment + box constraints), pick highest Sharpe.
min_var_weights <- function(Sigma, mu_vec, target_ret, max_w = 0.20) {
n <- length(mu_vec)
Dmat <- 2 * Sigma + diag(1e-8, n) # small ridge for numerical stability
dvec <- rep(0, n)
# Constraints (all as >= inequalities except the first two equalities):
# 1) sum(w) = 1 (equality)
# 2) mu'w = target_ret (equality)
# 3) w_i >= 0
# 4) w_i <= max_w → -w_i >= -max_w
Amat <- cbind(rep(1, n), # sum = 1
mu_vec, # return target
diag(n), # w >= 0
-diag(n)) # w <= max_w
bvec <- c(1, target_ret, rep(0, n), rep(-max_w, n))
tryCatch(
solve.QP(Dmat, dvec, Amat, bvec, meq = 2)$solution,
error = function(e) NULL
)
}
max_sharpe_weights <- function(ret_mat, rf_daily = 0.05 / 252, max_w = 0.20,
n_points = 200) {
mu <- colMeans(ret_mat)
Sigma <- cov(ret_mat)
n <- ncol(ret_mat)
# Feasible return range: equal-weight return ± a bit
ew_ret <- mean(mu)
lo <- min(mu) * 0.5
hi <- max(mu) * 0.9
targets <- seq(max(lo, ew_ret * 0.5), min(hi, ew_ret * 2), length.out = n_points)
best_sharpe <- -Inf
best_w <- rep(1 / n, n) # fallback: equal weight
for (tr in targets) {
w <- min_var_weights(Sigma, mu, tr, max_w)
if (is.null(w) || any(w < -1e-6) || abs(sum(w) - 1) > 1e-4) next
w <- pmax(w, 0); w <- w / sum(w)
sr <- (sum(w * mu) - rf_daily) / sqrt(t(w) %*% Sigma %*% w)
if (sr > best_sharpe) { best_sharpe <- sr; best_w <- w }
}
best_w
}
opt_w <- max_sharpe_weights(ret_mat)
names(opt_w) <- colnames(ret_mat)
tibble(Ticker = names(opt_w),
Weight = paste0(round(opt_w * 100, 2), "%")) |>
arrange(desc(opt_w)) |>
kable(caption = "Optimised Weights — Maximum Sharpe Ratio (max 20% per asset)")| Ticker | Weight |
|---|---|
| GOOGL | 20% |
| NVDA | 20% |
| JPM | 20% |
| V | 20% |
| AAPL | 9.79% |
| META | 6.3% |
| AMZN | 3.41% |
| UNH | 0.5% |
| MSFT | 0% |
| TSLA | 0% |
cum_ret <- cumprod(1 + combined) - 1
plot(cum_ret[, "AI_Portfolio"] * 100, type = "l", col = "#2196F3", lwd = 2,
main = "Cumulative Return: AI Portfolio vs S&P 500",
ylab = "Cumulative Return (%)", xlab = "")lines(cum_ret[, "Benchmark"] * 100, col = "#FF5722", lwd = 2, lty = 2)
legend("topleft", legend = c("AI Portfolio", "S&P 500 (SPY)"),
col = c("#2196F3", "#FF5722"), lwd = 2, lty = c(1, 2), bty = "n")
grid()charts.PerformanceSummary(
combined,
main = "Portfolio vs Benchmark — Returns, Drawdown & Distribution",
colorset = c("#2196F3", "#FF5722"),
lwd = 2
)table.AnnualizedReturns(combined, Rf = 0.05 / 252) |>
kable(digits = 4, caption = "Annualised Performance Metrics (Rf = 5%)")| AI_Portfolio | Benchmark | |
|---|---|---|
| Annualized Return | 0.4207 | 0.2285 |
| Annualized Std Dev | 0.2027 | 0.1513 |
| Annualized Sharpe (Rf=5%) | 1.7343 | 1.1141 |
table.Drawdowns(combined[, "AI_Portfolio"], top = 5) |>
kable(digits = 4, caption = "Top 5 Drawdowns — AI Portfolio")| From | Trough | To | Depth | Length | To Trough | Recovery |
|---|---|---|---|---|---|---|
| 2025-02-19 | 2025-04-08 | 2025-06-26 | -0.2290 | 89 | 35 | 54 |
| 2026-01-07 | 2026-03-27 | 2026-04-17 | -0.1327 | 70 | 56 | 14 |
| 2024-07-11 | 2024-08-05 | 2024-10-11 | -0.1251 | 66 | 18 | 48 |
| 2023-08-31 | 2023-10-27 | 2023-11-10 | -0.0948 | 51 | 41 | 10 |
| 2024-04-12 | 2024-04-19 | 2024-05-06 | -0.0652 | 17 | 6 | 11 |
tibble(
Metric = c("Alpha (annualised)", "Beta"),
Value = c(
round(CAPM.alpha(combined[,"AI_Portfolio"], combined[,"Benchmark"],
Rf = 0.05 / 252) * 252, 4),
round(CAPM.beta( combined[,"AI_Portfolio"], combined[,"Benchmark"],
Rf = 0.05 / 252), 4)
)
) |> kable(caption = "CAPM Alpha & Beta vs S&P 500")| Metric | Value |
|---|---|
| Alpha (annualised) | 0.1193 |
| Beta | 1.2110 |
What specific insights did AI provide that traditional analysis might have missed?
Do the backtesting results align with your initial hypothesis? If not, what are the potential causes? (e.g., look-ahead bias, regime change, no transaction costs modelled.)
| # | Prompt | AI Response Summary | Impact on Strategy |
|---|---|---|---|
| 1 | “Act as a Quant Researcher. Explain the rationale for Quality-Momentum…” | … | … |
| 2 | “From these 50 stocks, filter for FCF yield > 5% and D/E < 0.5…” | … | … |
| 3 | “Write R code for Max-Sharpe optimisation with a 20% cap per asset…” | … | … |
Rendered with tidyquant, quadprog, and
PerformanceAnalytics — all standard CRAN binaries.