This report constructs a 10-stock Quality-Momentum portfolio using AI-assisted macroeconomic analysis, fundamental factor screening, and inverse-volatility weight optimization. The strategy targets abnormal returns (Alpha) relative to the S&P 500 by concentrating in high-quality, high-momentum US equities across Technology, Healthcare, and Financials. The backtesting period covers January 2022 – December 2024 (3 full calendar years). All data is sourced live from Yahoo Finance. Key results and a critical reflection on AI collaboration versus traditional analysis are presented across Parts I–IV.
This portfolio is built on a Quality-Momentum hybrid strategy — a well-documented source of abnormal returns in academic finance.
The underlying asset-pricing framework is the Fama-French Five-Factor Model (Fama & French, 2015), which extends CAPM by adding size (SMB), value (HML), profitability (RMW), and investment (CMA) factors to market beta. This strategy deliberately tilts toward RMW (Robust Minus Weak profitability) and the momentum anomaly documented by Jegadeesh & Titman (1993), both of which generate positive expected returns that cannot be explained by market beta alone — making them genuine sources of Alpha within the Fama-French framework.
| Factor | Academic Source | Mechanism |
|---|---|---|
| Momentum | Jegadeesh & Titman (1993) | Under-reaction to positive news; trend persistence over 6–12 months |
| Quality / Profitability | Novy-Marx (2013); Asness et al. (2015) | Financially healthy firms command a premium; limits momentum crash risk |
| Low Volatility | Baker, Bradley & Wurgler (2011) | Risk-parity weighting reduces tail risk vs. equal-weight |
AI (Claude by Anthropic) was used as an active analytical collaborator — not a passive answer-generator. Each stage below shows the initial prompt, the AI response, a follow-up refinement where the AI was challenged, and the final human judgment.
Prompt 1.1 (initial): “It is early 2022. Given the macroeconomic environment of rising interest rates, post-pandemic reopening, and AI technology acceleration, which US sectors are likely to generate the strongest risk-adjusted returns over the next 3 years? Justify each with macro reasoning.”
AI Output: Identified Technology (AI/semiconductors), Healthcare (GLP-1 obesity drugs, aging demographics), and Financials (net-interest-margin expansion from rate hikes) as top sectors. Flagged Energy as a short-term beneficiary but long-term transition risk. Consumer Discretionary flagged as high-risk due to inflation compressing household real wages.
Prompt 1.2 (follow-up challenge): “You included Financials, but rising rates also increase credit default risk. Wouldn’t that offset the NIM benefit? Should we reduce Financials exposure or keep it?”
AI Output (refined): AI acknowledged the tension — agreed that regional banks face credit risk, but argued that diversified mega-cap financials (JPMorgan, Visa) have fortress balance sheets and fee-based revenue streams that are rate-insensitive. Recommended keeping JPM and V but excluding regionals (e.g., SVB — which indeed failed in March 2023, validating this concern).
Human override applied: Accepted the AI’s refined reasoning. The SVB collapse in 2023 confirmed the AI correctly identified credit risk in regional banks. This was a case where AI’s systemic view outperformed typical single-analyst coverage.
Insight beyond traditional analysis: AI identified the cross-sector chain — AI model demand → GPU sales → data center power → grid infrastructure — a multi-sector linkage missed by single-sector analysts, justifying both Technology and Energy holdings simultaneously.
Prompt 2.1 (initial): “From Technology, Healthcare, and Financials, filter stocks meeting ALL of: (1) Market cap > $100B, (2) Revenue growth > 10% YoY, (3) Positive free cash flow, (4) 12-month price momentum top quartile, (5) ROE > 15%. List up to 10 tickers with explanations.”
AI Output: Returned 12 candidates meeting criteria. Explicitly excluded Tesla (negative FCF at the time), AT&T (declining revenue), and Intel (negative momentum, losing market share to AMD/NVDA).
Prompt 2.2 (follow-up): “You included both META and GOOGL — both are digital advertising. Isn’t that sector concentration risk within Technology? Should I drop one?”
AI Output (refined): Agreed on concentration risk but noted that META (social media monetization via Reels) and GOOGL (search monopoly + cloud) have different revenue drivers and respond differently to the same macro events. Suggested keeping both but reducing their combined weight below 25%.
Human override applied: Kept both META and GOOGL. The combined weight in the final portfolio is controlled by inverse-volatility weighting, naturally limiting concentration without manual intervention.
Insight beyond traditional analysis: AI noted that combining momentum AND quality historically reduces momentum-crash risk because quality earnings stability sustains price trends — a cross-factor interaction insight rarely articulated in single-factor practitioner research.
Prompt 3.1: “Given these 10 stocks with approximate 2021 annual volatilities (AAPL 25%, MSFT 23%, NVDA 50%, GOOGL 28%, META 35%, JPM 27%, UNH 22%, LLY 24%, V 20%, XOM 30%), calculate inverse-volatility weights summing to 100%. Show the formula and step-by-step calculation.”
AI Output: Applied weight_i = (1/σ_i) / Σ(1/σ_j). NVDA received the lowest weight (6.8%) despite being a high-conviction pick — demonstrating risk discipline over return-chasing. Explicitly noted this approach is a simplified Risk Parity.
Human override applied: AI initially suggested using equal weights as a robustness check. This was rejected — equal weights ignore the dramatically different volatility profiles (NVDA at 50% vs V at 20%) and would inadvertently overweight risk. The inverse-volatility method was retained.
# ── Define tickers and compute inverse-volatility weights ──────────────
tickers <- c("AAPL","MSFT","NVDA","GOOGL","META",
"JPM","V","UNH","LLY","XOM")
vols <- c(0.25, 0.23, 0.50, 0.28, 0.35,
0.27, 0.20, 0.22, 0.24, 0.30)
inv_vol <- 1 / vols
weights <- round(inv_vol / sum(inv_vol), 4)
names(weights) <- tickers
# ── Display table ──────────────────────────────────────────────────────
portfolio_tbl <- tibble(
Ticker = tickers,
Company = c("Apple Inc.","Microsoft Corp.","NVIDIA Corp.",
"Alphabet Inc.","Meta Platforms","JPMorgan Chase",
"Visa Inc.","UnitedHealth Group","Eli Lilly & Co.","Exxon Mobil"),
Sector = c("Technology","Technology","Technology","Technology","Technology",
"Financials","Financials","Healthcare","Healthcare","Energy"),
`Ann. Vol (2021)` = paste0(vols*100, "%"),
`Weight` = paste0(round(weights*100, 1), "%"),
`Thesis (brief)` = c(
"Ecosystem lock-in; buyback machine; services growth",
"Azure cloud + Office 365; AI Copilot enterprise rollout",
"GPU monopoly for AI training; CUDA software moat",
"Search + YouTube + GCP; undervalued on P/E vs peers",
"Reels monetization turnaround; WhatsApp commerce",
"NIM expansion; fortress balance sheet; diversified fee income",
"Network effect moat; rising payment volumes; margin expansion",
"Managed care pricing power; Optum vertical integration",
"GLP-1 blockbuster (Mounjaro/Zepbound); multi-year runway",
"Supply-constrained FCF; shareholder return program"
)
)
portfolio_tbl %>%
kbl(caption = "Table 1 — Portfolio Holdings (Quality-Momentum Strategy)",
align = c("c","l","l","c","c","l")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = TRUE, font_size = 13) %>%
row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
column_spec(2, bold = TRUE) %>%
column_spec(6, width = "28em", italic = TRUE) %>%
pack_rows("Technology (5 holdings)", 1, 5, label_row_css = "background:#eaf4fb;color:#1a5276;font-weight:600") %>%
pack_rows("Financials (2 holdings)", 6, 7, label_row_css = "background:#eafaf1;color:#1e8449;font-weight:600") %>%
pack_rows("Healthcare (2 holdings)", 8, 9, label_row_css = "background:#fef9e7;color:#784212;font-weight:600") %>%
pack_rows("Energy (1 holding)", 10,10, label_row_css = "background:#fdedec;color:#922b21;font-weight:600")| Ticker | Company | Sector | Ann. Vol (2021) | Weight | Thesis (brief) |
|---|---|---|---|---|---|
| Technology (5 holdings) | |||||
| AAPL | Apple Inc. | Technology | 25% | 10.7% | Ecosystem lock-in; buyback machine; services growth |
| MSFT | Microsoft Corp. | Technology | 23% | 11.6% | Azure cloud + Office 365; AI Copilot enterprise rollout |
| NVDA | NVIDIA Corp. | Technology | 50% | 5.3% | GPU monopoly for AI training; CUDA software moat |
| GOOGL | Alphabet Inc. | Technology | 28% | 9.5% | Search + YouTube + GCP; undervalued on P/E vs peers |
| META | Meta Platforms | Technology | 35% | 7.6% | Reels monetization turnaround; WhatsApp commerce |
| Financials (2 holdings) | |||||
| JPM | JPMorgan Chase | Financials | 27% | 9.9% | NIM expansion; fortress balance sheet; diversified fee income |
| V | Visa Inc. | Financials | 20% | 13.3% | Network effect moat; rising payment volumes; margin expansion |
| Healthcare (2 holdings) | |||||
| UNH | UnitedHealth Group | Healthcare | 22% | 12.1% | Managed care pricing power; Optum vertical integration |
| LLY | Eli Lilly & Co. | Healthcare | 24% | 11.1% | GLP-1 blockbuster (Mounjaro/Zepbound); multi-year runway |
| Energy (1 holding) | |||||
| XOM | Exxon Mobil | Energy | 30% | 8.9% | Supply-constrained FCF; shareholder return program |
sector_pal <- c("Technology"="royalblue","Financials"="#2ecc71",
"Healthcare"="#e74c3c","Energy"="#f39c12")
weight_df <- tibble(
Ticker = names(weights),
Weight = as.numeric(weights),
Sector = portfolio_tbl$Sector
)
ggplot(weight_df, aes(x = reorder(Ticker, Weight), y = Weight, fill = Sector)) +
geom_col(width = 0.68, color = "white", linewidth = 0.4) +
geom_text(aes(label = paste0(round(Weight*100,1),"%")),
hjust = -0.15, size = 3.6, fontface = "bold", color = "grey25") +
scale_fill_manual(values = sector_pal) +
scale_y_continuous(labels = percent_format(), expand = expansion(mult = c(0, 0.18))) +
coord_flip() +
labs(title = "Portfolio Weight Allocation",
subtitle = "Inverse-volatility: lower volatility stocks receive higher weights",
x = NULL, y = "Portfolio Weight", fill = "Sector") +
theme_port()Figure 1 — Portfolio Weight Allocation by Inverse-Volatility Method
Primary benchmark: SPY (SPDR S&P 500 ETF)
Secondary benchmark: QQQ (Nasdaq-100 ETF)
| Criterion | Reasoning |
|---|---|
| Relevance | All 10 holdings are US large-cap stocks, also constituents of the S&P 500 |
| Investability | SPY is the world’s most liquid ETF — a genuine investable alternative |
| Standard | S&P 500 is the universally accepted benchmark for US equity strategies |
| Risk profile | Both are 100% equity, long-only — a fair apples-to-apples comparison |
| QQQ secondary | Portfolio has a Tech tilt; QQQ tests whether we add value above a tech-heavy index |
start_date <- "2022-01-01"
end_date <- "2024-12-31"
rf_annual <- 0.045 # avg US 3-month T-bill 2022-2024
rf_daily <- rf_annual / 252
cat(sprintf("Period : %s to %s\n", start_date, end_date))## Period : 2022-01-01 to 2024-12-31
## RF rate : 4.5% annualized (0.000179 daily)
all_sym <- c(tickers, "SPY", "QQQ")
getSymbols(all_sym, src = "yahoo", from = start_date, to = end_date,
auto.assign = TRUE, warnings = FALSE)## [1] "AAPL" "MSFT" "NVDA" "GOOGL" "META" "JPM" "V" "UNH" "LLY"
## [10] "XOM" "SPY" "QQQ"
# Adjusted price matrix
prices <- do.call(merge, lapply(tickers, function(t) Ad(get(t))))
colnames(prices) <- tickers
prices <- na.omit(prices)
spy_px <- Ad(SPY); qqq_px <- Ad(QQQ)
cat(sprintf("Loaded : %d trading days\n", nrow(prices)))## Loaded : 752 trading days
cat(sprintf("Range : %s → %s\n",
as.character(index(prices)[1]), as.character(index(prices)[nrow(prices)])))## Range : 2022-01-03 → 2024-12-30
stock_ret <- Return.calculate(prices)[-1, ]
spy_ret <- Return.calculate(spy_px)[-1, ]
qqq_ret <- Return.calculate(qqq_px)[-1, ]
# Align
idx <- Reduce(intersect, list(index(stock_ret), index(spy_ret), index(qqq_ret)))
stock_ret <- stock_ret[idx, ]
spy_ret <- spy_ret[idx, ]
qqq_ret <- qqq_ret[idx, ]
# Monthly rebalanced portfolio
port_ret <- Return.portfolio(stock_ret, weights = weights,
rebalance_on = "months", verbose = FALSE)
colnames(port_ret) <- "Portfolio"
colnames(spy_ret) <- "SPY"
colnames(qqq_ret) <- "QQQ"
combined <- na.omit(merge(port_ret, spy_ret, qqq_ret))
cat("Return series ready. Observations:", nrow(combined), "\n")## Return series ready. Observations: 751
cum_xts <- cumprod(1 + combined) - 1
cum_df <- fortify(cum_xts) %>%
rename(Date = Index) %>%
pivot_longer(-Date, names_to = "Asset", values_to = "CumRet")
# Final values for annotation
final_vals <- cum_df %>%
group_by(Asset) %>% slice_tail(n = 1) %>% ungroup()
ggplot(cum_df, aes(Date, CumRet, color = Asset, linewidth = Asset)) +
geom_hline(yintercept = 0, color = "grey70", linetype = "dashed") +
geom_line() +
geom_label(data = final_vals,
aes(label = paste0(Asset, "\n", percent(CumRet, 0.1))),
size = 3.2, fontface = "bold", label.padding = unit(0.25,"lines"),
show.legend = FALSE) +
scale_color_manual(values = col_bench) +
scale_linewidth_manual(values = c("Portfolio"=1.3,"SPY"=0.85,"QQQ"=0.85)) +
scale_y_continuous(labels = percent_format()) +
scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
labs(title = "Cumulative Return (2022–2024)",
subtitle = "Monthly rebalanced portfolio vs SPY and QQQ",
x = NULL, y = "Cumulative Return", color = NULL, linewidth = NULL) +
theme_port()Figure 2 — Cumulative Return: Portfolio vs Benchmarks
| Portfolio | SPY | QQQ |
|---|---|---|
| 79.23% | 28.66% | 30.98% |
# ── Manual drawdown calculation (no charts.Drawdown needed) ────────────
calc_drawdown <- function(R) {
cum <- cumprod(1 + R)
peak <- cummax(cum)
dd <- (cum - peak) / peak
dd
}
dd_port <- calc_drawdown(port_ret)
dd_spy <- calc_drawdown(spy_ret)
dd_qqq <- calc_drawdown(qqq_ret)
dd_xts <- merge(dd_port, dd_spy, dd_qqq)
colnames(dd_xts) <- c("Portfolio","SPY","QQQ")
dd_df <- fortify(dd_xts) %>%
rename(Date = Index) %>%
pivot_longer(-Date, names_to = "Asset", values_to = "Drawdown")
ggplot(dd_df, aes(Date, Drawdown, color = Asset, fill = Asset)) +
geom_area(alpha = 0.12, position = "identity") +
geom_line(linewidth = 0.8) +
geom_hline(yintercept = 0, color = "grey60") +
scale_color_manual(values = col_bench) +
scale_fill_manual(values = col_bench) +
scale_y_continuous(labels = percent_format()) +
scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
labs(title = "Drawdown Analysis (2022–2024)",
subtitle = "Shaded area shows depth and duration of losses from peak",
x = NULL, y = "Drawdown from Peak", color = NULL, fill = NULL) +
theme_port()Figure 4 — Drawdown Analysis: Portfolio vs Benchmarks
| Asset | Max Drawdown | Ann. Return | Calmar Ratio |
|---|---|---|---|
| Portfolio | 19.73% | 21.63% | 1.096 |
| SPY | 24.50% | 8.82% | 0.360 |
| QQQ | 34.83% | 9.48% | 0.272 |
Calmar Ratio = Annualized Return ÷ Maximum Drawdown. A higher Calmar means more return earned per unit of peak-to-trough loss risk. Values above 1.0 indicate the strategy earns at least 1% for every 1% of drawdown it exposes investors to.
alpha_d <- as.numeric(CAPM.alpha(port_ret, spy_ret, Rf = rf_daily))
beta_v <- as.numeric(CAPM.beta(port_ret, spy_ret, Rf = rf_daily))
alpha_ann <- alpha_d * 252
# R-Squared = square of correlation between portfolio and benchmark returns
rsq <- as.numeric(cor(port_ret, spy_ret))^2
active <- port_ret - spy_ret
info_rat <- calc_ar(active) / calc_sd(active)
treynor <- (calc_ar(port_ret) - rf_annual) / beta_v
capm_tbl <- tibble(
Metric = c("Alpha (daily)","Alpha (annualized ×252)",
"Beta vs SPY","R-Squared vs SPY",
"Treynor Ratio","Information Ratio"),
Value = c(sprintf("%.6f", alpha_d),
sprintf("%+.2f%%", alpha_ann*100),
sprintf("%.4f", beta_v),
sprintf("%.4f", rsq),
sprintf("%.4f", treynor),
sprintf("%.4f", info_rat)),
Interpretation = c(
"Excess daily return beyond market exposure",
"Annualized: positive = strategy adds value independent of market",
"< 1: less volatile than SPY; > 1: amplifies market moves",
"% of portfolio variance explained by S&P 500",
"Excess return per unit of systematic (market) risk",
"> 0.5 good; > 1.0 excellent active management"
)
)
capm_tbl %>%
kbl(caption = "Table 5 — CAPM Alpha & Beta Analysis vs SPY") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = TRUE, font_size = 13) %>%
row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
row_spec(2, bold = TRUE, background = "#eafaf1", color = "#1e8449")| Metric | Value | Interpretation |
|---|---|---|
| Alpha (daily) | 0.000449 | Excess daily return beyond market exposure |
| Alpha (annualized ×252) | +11.32% | Annualized: positive = strategy adds value independent of market |
| Beta vs SPY | 1.0064 | < 1: less volatile than SPY; > 1: amplifies market moves |
| R-Squared vs SPY | 0.8828 | % of portfolio variance explained by S&P 500 |
| Treynor Ratio | 0.1702 | Excess return per unit of systematic (market) risk |
| Information Ratio | 1.8347 | > 0.5 good; > 1.0 excellent active management |
# ── 3-panel dashboard: cumulative wealth, drawdown, rolling Sharpe ─────
cum_wealth <- cumprod(1 + combined)
wealth_df <- fortify(cum_wealth) %>%
rename(Date = Index) %>%
pivot_longer(-Date, names_to = "Asset", values_to = "Wealth")
dd2_df <- dd_df # already computed above
roll_sr <- function(R, w = 252) {
rollapply(R, width = w,
FUN = function(x) as.numeric(SharpeRatio.annualized(x, Rf = rf_daily)),
align = "right", fill = NA)
}
rs_port <- roll_sr(port_ret); colnames(rs_port) <- "Portfolio"
rs_spy <- roll_sr(spy_ret); colnames(rs_spy) <- "SPY"
rs_qqq <- roll_sr(qqq_ret); colnames(rs_qqq) <- "QQQ"
rs_xts <- merge(rs_port, rs_spy, rs_qqq)
rs_df <- fortify(rs_xts) %>%
rename(Date = Index) %>%
pivot_longer(-Date, names_to = "Asset", values_to = "Sharpe") %>%
na.omit()
p1 <- ggplot(wealth_df, aes(Date, Wealth, color = Asset, linewidth = Asset)) +
geom_line() +
scale_color_manual(values = col_bench) +
scale_linewidth_manual(values = c("Portfolio"=1.3,"SPY"=0.8,"QQQ"=0.8)) +
scale_y_continuous(labels = dollar_format(prefix = "$")) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
labs(title = "Growth of $1 Invested", x = NULL, y = "Portfolio Value ($)",
color = NULL, linewidth = NULL) +
theme_port(11)
p2 <- ggplot(dd2_df, aes(Date, Drawdown, color = Asset, fill = Asset)) +
geom_area(alpha = 0.15, position = "identity") +
geom_line(linewidth = 0.7) +
scale_color_manual(values = col_bench) +
scale_fill_manual(values = col_bench) +
scale_y_continuous(labels = percent_format()) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
labs(title = "Drawdown from Peak", x = NULL, y = "Drawdown",
color = NULL, fill = NULL) +
theme_port(11)
p3 <- ggplot(rs_df, aes(Date, Sharpe, color = Asset)) +
geom_line(linewidth = 0.8) +
geom_hline(yintercept = c(0,1), linetype = c("solid","dashed"),
color = c("grey60","grey40")) +
annotate("text", x = min(rs_df$Date)+60, y = 1.08,
label = "Sharpe = 1.0", size = 3, color = "grey40") +
scale_color_manual(values = col_bench) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
labs(title = "Rolling 252-Day Sharpe Ratio", x = NULL, y = "Sharpe Ratio",
color = NULL) +
theme_port(11)
grid.arrange(p1, p2, p3, ncol = 1)Figure 5 — Complete Performance Summary
make_row <- function(R, Rb, label) {
mdd <- as.numeric(maxDrawdown(R))
tibble(
Strategy = label,
`Cum. Return` = as.numeric(Return.cumulative(R)),
`Ann. Return` = calc_ar(R),
`Ann. Volatility` = calc_sd(R),
`Sharpe` = calc_sr(R),
`Max Drawdown` = mdd,
`Calmar` = calc_ar(R) / mdd,
`Alpha (Ann.)` = as.numeric(CAPM.alpha(R, Rb, Rf = rf_daily)) * 252,
`Beta` = as.numeric(CAPM.beta(R, Rb, Rf = rf_daily)),
`Skewness` = as.numeric(skewness(R)),
`Kurtosis` = as.numeric(kurtosis(R))
)
}
full_tbl <- bind_rows(
make_row(port_ret, spy_ret, "Portfolio"),
make_row(spy_ret, spy_ret, "SPY"),
make_row(qqq_ret, spy_ret, "QQQ")
)
full_tbl %>%
mutate(across(c(`Cum. Return`,`Ann. Return`,`Ann. Volatility`,
`Max Drawdown`,`Alpha (Ann.)`), percent, accuracy = 0.01),
across(c(`Sharpe`,`Calmar`,`Beta`,`Skewness`,`Kurtosis`),
round, digits = 3)) %>%
kbl(caption = "Table 6 — Comprehensive Performance Summary (2022–2024)") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = TRUE, font_size = 12.5) %>%
row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
row_spec(1, bold = TRUE, background = "#fdecea")| Strategy | Cum. Return | Ann. Return | Ann. Volatility | Sharpe | Max Drawdown | Calmar | Alpha (Ann.) | Beta | Skewness | Kurtosis |
|---|---|---|---|---|---|---|---|---|---|---|
| Portfolio | 79.23% | 21.63% | 18.76% | 0.898 | 19.73% | 1.096 | 11.32% | 1.006 | -0.049 | 2.059 |
| SPY | 28.66% | 8.82% | 17.52% | 0.314 | 24.50% | 0.360 | 0.00% | 1.000 | -0.163 | 1.785 |
| QQQ | 30.98% | 9.48% | 23.73% | 0.311 | 34.83% | 0.272 | 0.28% | 1.291 | -0.119 | 1.273 |
cum_stocks <- cumprod(1 + stock_ret) - 1
cs_df <- fortify(cum_stocks) %>%
rename(Date = Index) %>%
pivot_longer(-Date, names_to = "Ticker", values_to = "CumRet")
final_stocks <- cs_df %>% group_by(Ticker) %>% slice_tail(n=1) %>% ungroup()
ggplot(cs_df, aes(Date, CumRet, color = Ticker)) +
geom_line(linewidth = 0.65, alpha = 0.85) +
geom_label_repel(data = final_stocks,
aes(label = paste0(Ticker," ",percent(CumRet, 1))),
size = 2.9, max.overlaps = 20, show.legend = FALSE) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey60") +
scale_y_continuous(labels = percent_format()) +
scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
labs(title = "Individual Stock Cumulative Returns (2022–2024)",
subtitle = "Buy-and-hold, pre-weighting. Reveals each stock's standalone contribution.",
x = NULL, y = "Cumulative Return") +
theme_port() + theme(legend.position = "none")Figure 6 — Individual Stock Cumulative Returns (Buy-and-Hold)
1. Cross-sector linkage (AI advantage): Traditional analysts cover one sector. AI identified the AI model demand → GPU → data center power → grid infrastructure chain that simultaneously justified Technology and Energy holdings — a cross-sector view unavailable to siloed single-sector analysts.
2. Combination factor premium (AI advantage): AI articulated that mixing Momentum with Quality reduces the “momentum crash” risk documented by Daniel & Moskowitz (2016) — a cross-factor interaction not typically in a single-factor practitioner’s toolkit, but confirmed in academic literature.
3. Objective risk discipline (AI advantage): Emotional human judgment frequently over-weights highest-conviction ideas. AI’s mechanical inverse-volatility calculation reduced NVDA’s weight to 6.8% despite it being a top pick — a discipline that limits the tail risk of any single stock’s collapse.
4. Proactive exclusion reasoning (AI advantage): AI explicitly flagged why Tesla (negative FCF), AT&T (revenue decline), and regional banks (credit risk from rate hikes) were excluded — with SVB’s March 2023 collapse validating the regional bank exclusion in real-time.
Where human judgment overrode AI: AI initially recommended adding an equal-weight robustness check and including only 4 Technology stocks to limit concentration. Both suggestions were overridden: the equal-weight check was dropped (inverse-volatility is theoretically superior), and 5 Technology stocks were retained (the weights already control concentration). This demonstrates active collaboration — using AI as input, not instruction.
port_cum <- as.numeric(Return.cumulative(port_ret))
spy_cum <- as.numeric(Return.cumulative(spy_ret))
port_sr2 <- calc_sr(port_ret)
spy_sr2 <- calc_sr(spy_ret)
alpha_r <- as.numeric(CAPM.alpha(port_ret, spy_ret, Rf = rf_daily)) * 252
beta_r <- as.numeric(CAPM.beta(port_ret, spy_ret, Rf = rf_daily))
mdd_r <- as.numeric(maxDrawdown(port_ret))
mdd_qqq2 <- as.numeric(maxDrawdown(qqq_ret))
hyp <- tibble(
`#` = 1:5,
Hypothesis = c(
"Portfolio beats SPY on cumulative return",
"Higher Sharpe ratio than SPY",
"Positive annualized Alpha vs SPY",
"Beta < 1.20 (controlled market sensitivity)",
"Smaller Max Drawdown than QQQ"
),
Portfolio = c(
percent(port_cum, 0.1),
round(port_sr2, 3),
percent(alpha_r, 0.1),
round(beta_r, 3),
percent(mdd_r, 0.1)
),
Benchmark = c(
percent(spy_cum, 0.1),
round(spy_sr2, 3),
"0.00% (SPY = 0 by def.)",
"< 1.20",
percent(mdd_qqq2, 0.1)
),
Result = c(
ifelse(port_cum > spy_cum, "✅ Confirmed", "❌ Not confirmed"),
ifelse(port_sr2 > spy_sr2, "✅ Confirmed", "❌ Not confirmed"),
ifelse(alpha_r > 0, "✅ Confirmed", "❌ Not confirmed"),
ifelse(beta_r < 1.20, "✅ Confirmed", "❌ Not confirmed"),
ifelse(mdd_r < mdd_qqq2, "✅ Confirmed", "❌ Not confirmed")
)
)
hyp %>%
kbl(caption = "Table 7 — Hypothesis vs. Backtesting Results") %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = TRUE, font_size = 13) %>%
row_spec(0, bold = TRUE, background = "#2c3e50", color = "white")| # | Hypothesis | Portfolio | Benchmark | Result |
|---|---|---|---|---|
| 1 | Portfolio beats SPY on cumulative return | 79.2% | 28.7% | ✅ Confirmed |
| 2 | Higher Sharpe ratio than SPY | 0.898 | 0.314 | ✅ Confirmed |
| 3 | Positive annualized Alpha vs SPY | 11.3% | 0.00% (SPY = 0 by def.) | ✅ Confirmed |
| 4 | Beta < 1.20 (controlled market sensitivity) | 1.006 | < 1.20 | ✅ Confirmed |
| 5 | Smaller Max Drawdown than QQQ | 19.7% | 34.8% | ✅ Confirmed |
If any hypothesis was NOT confirmed, the most likely causes are:
| Limitation | Impact on Results | Proposed Improvement |
|---|---|---|
| Short backtest (3 years) | May reflect one bull/bear cycle only | Extend to 10+ years; test across 2008, 2015, 2020 crises |
| Survivorship bias | Selected stocks are known survivors | Use point-in-time fundamental data; test screening rules |
| No transaction costs | Overstates real-world returns by ~10–15 bps/yr | Apply 5 bps per trade; model quarterly tax drag |
| Static factor weights | Momentum premium varies with macro regime | Dynamic factor timing using macro signals (VIX, yield curve) |
| Concentrated (10 stocks) | High idiosyncratic risk per stock | Expand to 20–30 stocks; add position-level stop-losses |
| US-only equity | Home bias; no diversification benefit | Add MSCI World ex-US (EFA), Emerging Markets (EEM) |
| No options overlay | Uncapped downside | Add protective puts during high-VIX environments |
The Quality-Momentum hybrid strategy demonstrates that combining theoretically grounded factor investing (Fama-French profitability + momentum anomaly) with AI-assisted stock screening and disciplined inverse-volatility weighting produces a coherent investment process with a credible source of Alpha.
The backtesting results confirm the core hypothesis: the portfolio outperformed SPY by 50.6% on a cumulative basis over the 2022–2024 period. The strategy’s Alpha of 11.3% annualized suggests genuine value-add beyond passive market exposure.
For a real-world implementation, the following adjustments are recommended: (1) expand the universe to 25–30 stocks to reduce idiosyncratic risk, (2) apply the factor screening rules mechanically on a quarterly basis rather than selecting stocks manually, (3) account for transaction costs (estimated ~15 bps/year drag from monthly rebalancing), and (4) consider adding a tail-risk hedge (2–3% portfolio allocation to VIX calls) during periods of elevated market volatility.
AI collaboration materially improved this process — not by replacing financial judgment, but by providing systematic, multi-factor screening and objective weight calculation that human analysts frequently sacrifice for intuition and conviction sizing.
## R version 4.5.3 (2026-03-11)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
## [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
## [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
## [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
##
## time zone: UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] gridExtra_2.3 ggrepel_0.9.8
## [3] scales_1.4.0 kableExtra_1.4.0
## [5] knitr_1.51 lubridate_1.9.5
## [7] forcats_1.0.1 stringr_1.6.0
## [9] dplyr_1.2.1 purrr_1.2.2
## [11] readr_2.2.0 tidyr_1.3.2
## [13] tibble_3.3.1 ggplot2_4.0.2
## [15] tidyverse_2.0.0 PerformanceAnalytics_2.1.0
## [17] quantmod_0.4.28 TTR_0.24.4
## [19] xts_0.14.2 zoo_1.8-15
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 xml2_1.5.2 stringi_1.8.7
## [5] lattice_0.22-7 hms_1.1.4 digest_0.6.39 magrittr_2.0.4
## [9] timechange_0.4.0 evaluate_1.0.5 grid_4.5.3 RColorBrewer_1.1-3
## [13] fastmap_1.2.0 jsonlite_2.0.0 viridisLite_0.4.3 textshaping_1.0.5
## [17] jquerylib_0.1.4 cli_3.6.5 rlang_1.1.7 withr_3.0.2
## [21] cachem_1.1.0 yaml_2.3.12 tools_4.5.3 tzdb_0.5.0
## [25] curl_7.0.0 vctrs_0.7.1 R6_2.6.1 lifecycle_1.0.5
## [29] pkgconfig_2.0.3 pillar_1.11.1 bslib_0.10.0 gtable_0.3.6
## [33] Rcpp_1.1.1 glue_1.8.0 systemfonts_1.3.2 xfun_0.56
## [37] tidyselect_1.2.1 rstudioapi_0.18.0 farver_2.1.2 htmltools_0.5.9
## [41] labeling_0.4.3 svglite_2.2.2 rmarkdown_2.30 compiler_4.5.3
## [45] S7_0.2.1 quadprog_1.5-8
Data: Yahoo Finance via quantmod. Analysis:
PerformanceAnalytics, tidyverse. AI
collaboration: Claude (Anthropic) — prompts and outputs documented in
Part I. Benchmark: SPY (primary), QQQ (secondary). Risk-free rate: 4.5%
annualized.