Part I: Investment Thesis & Logic

Strategy Description

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.”)

AI Collaboration Process

Document how AI was used:

  • What prompts did you use?
  • How did AI help narrow the stock universe or optimise weights?

Part II: Portfolio Construction

Step 1 — Load Required Libraries

# Run this in Console once to install:
# install.packages(c("tidyquant", "quadprog", "PerformanceAnalytics", "knitr"))

library(tidyquant)
library(quadprog)
library(PerformanceAnalytics)
library(tidyverse)
library(knitr)

Step 2 — Define Assets and Benchmark

# AI-selected tickers (max 10) — replace with your own
tickers          <- c("AAPL", "MSFT", "GOOGL", "AMZN", "TSLA",
                      "META", "NVDA", "JPM", "V", "UNH")
benchmark_ticker <- "SPY"

Asset Table

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")
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%

Part III: Backtesting & Performance Analysis

Step 3 — Download 3 Years of Price Data

end_date   <- Sys.Date()
start_date <- end_date - lubridate::years(3)

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")

Step 4 — Calculate Daily Returns

# 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()

Step 5 — Portfolio Optimisation (Maximum Sharpe Ratio via quadprog)

# ── 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)")
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%

Step 6 — Calculate Portfolio Returns

# Weighted daily portfolio return
port_ret <- as.numeric(ret_mat %*% opt_w)

port_xts  <- xts(data.frame(AI_Portfolio = port_ret), order.by = dates)
bench_xts <- xts(data.frame(Benchmark    = bench_ret$Benchmark),
                 order.by = bench_ret$date)

combined <- merge(port_xts, bench_xts) |> na.omit()

Cumulative Performance Chart

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()

Performance Summary Chart

charts.PerformanceSummary(
  combined,
  main     = "Portfolio vs Benchmark — Returns, Drawdown & Distribution",
  colorset = c("#2196F3", "#FF5722"),
  lwd      = 2
)

Annualised Metrics Table

table.AnnualizedReturns(combined, Rf = 0.05 / 252) |>
  kable(digits = 4, caption = "Annualised Performance Metrics (Rf = 5%)")
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

Maximum Drawdown

table.Drawdowns(combined[, "AI_Portfolio"], top = 5) |>
  kable(digits = 4, caption = "Top 5 Drawdowns — AI Portfolio")
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

Sharpe Ratio

SharpeRatio.annualized(combined, Rf = 0.05 / 252) |>
  kable(digits = 4, caption = "Annualised Sharpe Ratio")
Annualised Sharpe Ratio
AI_Portfolio Benchmark
Annualized Sharpe Ratio (Rf=5%, p=95%): 1.5877 1.105

Alpha & Beta

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")
CAPM Alpha & Beta vs S&P 500
Metric Value
Alpha (annualised) 0.1193
Beta 1.2110

Part IV: Critical Reflection

AI Insights

What specific insights did AI provide that traditional analysis might have missed?

Hypothesis vs Results

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.)


Appendix: AI Prompt Log

# 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.