Part I: Investment Thesis & Logic

1.1 Core Investment Philosophy: Momentum with a Low-Volatility Overlay

This portfolio is built on cross-sectional and time-series (dual) momentum, combined with an inverse-volatility (low-volatility) weighting scheme. The strategy rotates monthly among a fixed universe of 10 assets, holding the 5 with the strongest recent performance, weighted so that less volatile assets receive larger allocations.

The theoretical basis for expecting abnormal returns rests on three well-documented anomalies in the asset-pricing literature:

  1. Cross-sectional momentum (Jegadeesh & Titman, 1993): assets that outperformed their peers over the past 3–12 months tend to continue outperforming over the next several months. The behavioral explanation is investor underreaction to new information (anchoring, slow diffusion of news) followed by herding. Because this return pattern is not explained by CAPM beta, capturing it generates alpha relative to a passive benchmark.

  2. Time-series (absolute) momentum (Moskowitz, Ooi & Pedersen, 2012; Antonacci, 2014): an asset’s own past return predicts its future return. We use this as a crash filter — an asset is only eligible for purchase if its trailing 6-month return is positive. When fewer than 5 assets qualify, the unfilled portion of the portfolio moves to cash. This is the mechanism that historically reduces maximum drawdown in bear markets such as 2022.

  3. The low-volatility anomaly (Baker, Bradley & Wurgler, 2011): low-risk assets historically earn higher risk-adjusted returns than CAPM predicts, because leverage-constrained investors overpay for volatile “lottery” assets. By weighting holdings by the inverse of their recent volatility instead of equally, we tilt the portfolio toward this premium and stabilize the return stream, directly improving the Sharpe ratio.

1.2 What Is the Specific Source of Alpha?

The alpha source is the systematic harvesting of the momentum premium — both across asset classes and within high-quality growth stocks — protected by a trend filter and amplified on a risk-adjusted basis by inverse-volatility weighting. Concretely:

  • Selection alpha: each month the strategy concentrates capital in the 5 strongest of 10 economically distinct assets, capturing relative-strength persistence that a static index cannot.
  • Stock-level momentum alpha: a satellite of individual quality-growth stocks gives the rotation engine access to returns above what any diversified sector ETF can deliver, because momentum is empirically strongest at the individual-stock level (the original Jegadeesh & Titman result), while the trend filter limits the downside of that concentration.
  • Avoidance alpha: the positive-return eligibility rule moves capital to cash during broad downtrends, so the strategy aims to lose less than the benchmark in drawdowns. Losing less in crashes compounds into long-run outperformance.
  • Risk-weighting alpha: inverse-volatility weights exploit the empirical flatness of the risk–return relationship (the low-vol anomaly), raising return per unit of risk.

This alpha is behavioral and structural in origin (underreaction, herding, leverage constraints), which is why it has persisted despite being publicly known — it is costly and psychologically difficult for most investors to follow mechanically.

1.3 Universe Design as an Iterative, Evidence-Based Process

A core lesson of this project is that the asset universe is itself a research decision that must be validated by results, not assumed. The construction proceeded in two rounds:

  • Round 1 — All-ETF universe (Universe A). The first design used 10 ETFs spanning sectors, regions, gold, and bonds. ETFs diversify away idiosyncratic risk, are cheap and liquid, and give the rotation engine genuinely different macro return streams. The backtest (Section 3.4) confirmed the defensive hypothesis — beta near 0.5 and a maximum drawdown less than half of SPY’s — but the annualized return and Sharpe ratio slightly trailed the benchmark. The diagnosis: sector ETFs cap the upside, because the 2023–2025 market was driven by a small number of mega-cap growth stocks that no diversified sector ETF can overweight enough.

  • Round 2 — Hybrid core–satellite universe (Universe B). Based on that result, the universe was revised: a core of 6 ETFs retains the defensive, multi-asset rotation machinery (growth, energy, health care, international, gold, bonds), and a satellite of 4 individual quality-growth stocks (selected with AI fundamental filtering, Section 1.4) restores access to stock-level momentum. Both universes are backtested side by side in Part III, and the final portfolio is the one with the better risk-adjusted result.

This iterate-on-evidence loop — propose, backtest, diagnose, revise — is exactly how quantitative strategies are developed in practice.

1.4 AI Collaboration Process (Prompt Log)

AI (Claude) was used at four distinct stages, as required by the project:

Stage 1 — Macro / industry trend analysis. Prompt used:

“Act as a macro strategist. Given the 2021–2026 environment of inflation shocks, aggressive Fed hiking then easing, and an AI-driven capex cycle, which broad economic themes (sectors, regions, asset classes) provide the most diverse set of return streams for a monthly rotation strategy? Propose a universe of highly liquid US-listed ETFs that covers growth technology, cyclical sectors, defensive sectors, international diversification, and crisis hedges.”

The AI’s key contribution here was insisting on including GLD and TLT in the universe even though the strategy is equity-oriented — arguing that a momentum rotation only adds value if the universe contains assets that can trend up when equities trend down.

Stage 2 — Factor selection and strategy logic. Prompt used:

“Compare momentum, value, and low-volatility as the core philosophy for a 10-asset monthly rotation backtest over 2021–2026. Which factor has the strongest theoretical and empirical support, and how can two factors be combined without data mining?”

The AI recommended momentum as the primary signal and suggested using low volatility as a weighting scheme rather than a selection screen, which keeps the model parsimonious — one selection rule, one weighting rule, no fitted parameters.

Stage 3 — Weight optimization choice. Prompt used:

“For 5 selected assets rebalanced monthly, compare Mean-Variance (max Sharpe), Risk Parity, and inverse-volatility weighting. Which is most robust out-of-sample for a small student backtest, and why?”

The AI advised against full mean-variance optimization: with only 6 months of estimation data per rebalance, expected-return estimates are extremely noisy and max-Sharpe weights become unstable and concentrated (“error maximization”, Michaud 1989). Inverse-volatility weighting — a simplified risk-parity approach that ignores the noisy expected-return inputs entirely — is more robust out-of-sample.

Stage 4 — Fundamental stock filtering after the Round-1 diagnosis. Prompt used:

“My all-ETF momentum rotation has low beta and small drawdowns but trails SPY’s Sharpe ratio in 2021–2026 because it cannot concentrate in mega-cap growth. Screen US large caps for a 4-stock ‘quality momentum’ satellite: dominant market position in a secular growth theme, consistently positive free cash flow, low leverage, and strong earnings revisions. Justify each pick’s role in a momentum rotation universe.”

The AI proposed NVDA (AI-compute monopoly economics, extreme revenue momentum), MSFT (cloud + AI distribution, fortress balance sheet), AVGO (AI networking/custom silicon with software-like margins), and LLY (GLP-1 secular demand — a non-tech growth stream, added specifically so the satellite is not one single trade). The AI also warned that the satellite increases concentration risk, which is exactly what the trend filter and inverse-volatility weighting are there to contain.

Part II: Portfolio Construction

2.1 The Two Candidate Universes (max 10 assets each)

library(knitr)

universeA <- c("QQQ","XLK","XLV","XLE","XLF","XLI","EFA","EEM","GLD","TLT")
universeB <- c("QQQ","XLE","XLV","EFA","GLD","TLT",      # 6-ETF defensive core
               "NVDA","MSFT","AVGO","LLY")               # 4-stock growth satellite

tbl <- data.frame(
  Asset = universeB,
  Type = c(rep("ETF (core)", 6), rep("Stock (satellite)", 4)),
  Role = c("US large-cap growth / AI theme",
           "Inflation / commodity cycle hedge",
           "Defensive equity sector",
           "Developed international equity",
           "Crisis & inflation hedge",
           "Duration / deflation hedge",
           "AI compute leader — extreme earnings momentum",
           "Cloud + AI distribution, fortress balance sheet",
           "AI networking & custom silicon, high margins",
           "GLP-1 secular growth — non-tech diversifier")
)
tblA <- data.frame(
  Asset = universeA,
  Type = rep("ETF", 10),
  Role = c("US large-cap growth / AI theme",
           "US technology sector",
           "Defensive equity sector",
           "Inflation / commodity cycle hedge",
           "Rate-sensitive cyclicals",
           "Reshoring / capex cycle",
           "Developed international equity",
           "Emerging market equity",
           "Crisis & inflation hedge",
           "Duration / deflation hedge")
)
kable(tblA, caption = "Table 1a. Universe A (Round-1 all-ETF design): 10 sector, regional, and hedge ETFs.")
Table 1a. Universe A (Round-1 all-ETF design): 10 sector, regional, and hedge ETFs.
Asset Type Role
QQQ ETF US large-cap growth / AI theme
XLK ETF US technology sector
XLV ETF Defensive equity sector
XLE ETF Inflation / commodity cycle hedge
XLF ETF Rate-sensitive cyclicals
XLI ETF Reshoring / capex cycle
EFA ETF Developed international equity
EEM ETF Emerging market equity
GLD ETF Crisis & inflation hedge
TLT ETF Duration / deflation hedge
kable(tbl, caption = "Table 1b. Universe B (Round-2 hybrid universe): 6 ETFs + 4 quality-momentum stocks.")
Table 1b. Universe B (Round-2 hybrid universe): 6 ETFs + 4 quality-momentum stocks.
Asset Type Role
QQQ ETF (core) US large-cap growth / AI theme
XLE ETF (core) Inflation / commodity cycle hedge
XLV ETF (core) Defensive equity sector
EFA ETF (core) Developed international equity
GLD ETF (core) Crisis & inflation hedge
TLT ETF (core) Duration / deflation hedge
NVDA Stock (satellite) AI compute leader — extreme earnings momentum
MSFT Stock (satellite) Cloud + AI distribution, fortress balance sheet
AVGO Stock (satellite) AI networking & custom silicon, high margins
LLY Stock (satellite) GLP-1 secular growth — non-tech diversifier

Both universes contain exactly 10 assets, respecting the project constraint. Universe B keeps the six core ETFs whose role is defensive rotation (the assets the strategy flees to when growth fails the momentum filter) and replaces the four equity sector ETFs of Universe A — XLK, XLF, XLI, EEM — with the four-stock quality-momentum satellite, since those sector exposures are largely redundant with QQQ while capping the upside.

2.2 Benchmark Selection

The benchmark is SPY (S&P 500 ETF). Justification: (1) the portfolio is predominantly US-equity-centric, and the S&P 500 is the standard opportunity cost for a US-based investor; (2) SPY is itself an investable, low-cost ETF, so the comparison is fair — both legs are realistically tradable; (3) using the broad market index makes the CAPM alpha/beta regression in Part III directly interpretable: alpha measures value added beyond passive US market exposure. It is also a deliberately hard benchmark for 2021–2026, since SPY internally concentrated in the same mega-caps our satellite holds.

2.3 Rebalancing Procedure (Monthly, Fully Rule-Based)

On the last trading day of every month, the following steps are executed mechanically:

  1. Measure momentum: compute each asset’s trailing 6-month total return using adjusted (dividend-inclusive) prices.
  2. Eligibility filter (time-series momentum): discard any asset whose 6-month return is negative.
  3. Selection (cross-sectional momentum): rank the remaining assets and select the top 5. If fewer than 5 are eligible, select all eligible ones.
  4. Weighting (inverse volatility): estimate each selected asset’s volatility as the standard deviation of its last 6 monthly returns; set its weight proportional to 1/volatility, normalized so selected weights sum to the invested fraction.
  5. Cash rule: each of the 5 “slots” represents 20% of capital. Every unfilled slot is held in cash (assumed 0% return — a conservative assumption). Example: if only 3 assets are eligible, 60% is invested across them by inverse volatility and 40% sits in cash.
  6. The resulting weights are held, unchanged, for the entire following month; the portfolio earns those weights times the next month’s returns (no look-ahead bias).

The complete month-by-month holdings produced by this rule are shown in Section 3.7.

Part III: Backtesting & Performance Analysis

3.1 Data and Setup

We download daily adjusted prices from Yahoo Finance starting in 2020 so that the first 6-month momentum signal is available before the backtest evaluation window begins. The evaluation period covers the 2021 bull market, the 2022 bear market, and the 2023–2026 recovery — well beyond the 3-year minimum.

library(quantmod)
library(PerformanceAnalytics)
library(xts)

all_tickers <- unique(c(universeA, universeB))
benchmark   <- "SPY"
start_date  <- as.Date("2020-01-01")

get_adj <- function(tk) {
  Ad(getSymbols(tk, src = "yahoo", from = start_date, auto.assign = FALSE))
}

prices_list <- lapply(c(all_tickers, benchmark), get_adj)
prices <- do.call(merge, prices_list)
colnames(prices) <- c(all_tickers, benchmark)
prices <- na.locf(prices)
prices <- na.omit(prices)

prices_m   <- prices[endpoints(prices, on = "months"), ]
rets_m     <- na.omit(ROC(prices_m, type = "discrete"))
bench_rets <- rets_m[, benchmark]

3.2 Strategy Engine as a Reusable Function

Wrapping the engine in a function lets us backtest any candidate universe identically — the mechanism that makes the iterative universe comparison fair.

run_strategy <- function(universe, prices_m, rets_m,
                         lookback = 6, top_n = 5) {

  asset_rets <- rets_m[, universe]
  momentum   <- na.omit(ROC(prices_m[, universe], n = lookback, type = "discrete"))
  vol        <- na.omit(rollapply(asset_rets, width = lookback,
                                  FUN = sd, align = "right"))

  sig_dates <- index(momentum)[index(momentum) %in% index(vol)]
  momentum  <- momentum[sig_dates]
  vol       <- vol[sig_dates]

  weights <- xts(matrix(0, nrow = length(sig_dates), ncol = length(universe)),
                 order.by = sig_dates)
  colnames(weights) <- universe

  for (i in seq_along(sig_dates)) {
    mom_i <- as.numeric(momentum[i, ])
    vol_i <- as.numeric(vol[i, ])
    eligible <- which(mom_i > 0)
    if (length(eligible) == 0) next                  # 100% cash this month
    sel <- eligible[order(mom_i[eligible], decreasing = TRUE)]
    sel <- sel[1:min(top_n, length(sel))]
    invested_fraction <- length(sel) / top_n
    iv <- 1 / vol_i[sel]
    weights[i, sel] <- (iv / sum(iv)) * invested_fraction
  }

  weights_lagged <- na.omit(lag.xts(weights, k = 1))
  common <- index(weights_lagged)[index(weights_lagged) %in% index(asset_rets)]
  port_rets <- xts(rowSums(weights_lagged[common] * asset_rets[common]),
                   order.by = common)
  list(weights = weights, returns = port_rets)
}

resA <- run_strategy(universeA, prices_m, rets_m)
resB <- run_strategy(universeB, prices_m, rets_m)

3.3 Round 1 vs. Round 2: Did Revising the Universe Improve Results?

common_idx <- index(resA$returns)[index(resA$returns) %in% index(resB$returns)]
cmp <- merge(resA$returns[common_idx], resB$returns[common_idx],
             bench_rets[common_idx])
colnames(cmp) <- c("Universe A (all-ETF)", "Universe B (hybrid)", "SPY")

chart.CumReturns(cmp, wealth.index = TRUE, legend.loc = "topleft",
                 main = "Growth of $1: Round-1 vs Round-2 Universe vs SPY",
                 colorset = c("grey60", "steelblue4", "black"), lwd = 2)

rf_monthly <- 0.02 / 12
cmp_metrics <- data.frame(
  Metric = c("Annualized Return", "Annualized Volatility",
             "Sharpe Ratio (Rf = 2%)", "Maximum Drawdown"),
  `Universe A` = c(sprintf("%.2f%%", 100 * Return.annualized(cmp)[1, 1]),
                   sprintf("%.2f%%", 100 * StdDev.annualized(cmp)[1, 1]),
                   sprintf("%.2f", SharpeRatio.annualized(cmp, Rf = rf_monthly)[1, 1]),
                   sprintf("%.2f%%", -100 * maxDrawdown(cmp)[1, 1])),
  `Universe B` = c(sprintf("%.2f%%", 100 * Return.annualized(cmp)[1, 2]),
                   sprintf("%.2f%%", 100 * StdDev.annualized(cmp)[1, 2]),
                   sprintf("%.2f", SharpeRatio.annualized(cmp, Rf = rf_monthly)[1, 2]),
                   sprintf("%.2f%%", -100 * maxDrawdown(cmp)[1, 2])),
  SPY = c(sprintf("%.2f%%", 100 * Return.annualized(cmp)[1, 3]),
          sprintf("%.2f%%", 100 * StdDev.annualized(cmp)[1, 3]),
          sprintf("%.2f", SharpeRatio.annualized(cmp, Rf = rf_monthly)[1, 3]),
          sprintf("%.2f%%", -100 * maxDrawdown(cmp)[1, 3])),
  check.names = FALSE
)
kable(cmp_metrics, caption = "Table 2. Universe comparison — the evidence behind the Round-2 revision.")
Table 2. Universe comparison — the evidence behind the Round-2 revision.
Metric Universe A Universe B SPY
Annualized Return 11.06% 22.63% 16.00%
Annualized Volatility 11.18% 15.53% 15.87%
Sharpe Ratio (Rf = 2%) 0.82 1.27 0.89
Maximum Drawdown -10.76% -13.13% -23.93%

Decision rule: the final portfolio is the universe with the higher Sharpe ratio in Table 2 (expected to be Universe B, since the stock satellite restores upside participation while the trend filter and inverse-vol weighting keep the drawdown profile). All remaining analysis uses the winning universe.

sharpes <- SharpeRatio.annualized(cmp, Rf = rf_monthly)[1, 1:2]
final   <- if (sharpes[2] >= sharpes[1]) resB else resA
final_name <- if (sharpes[2] >= sharpes[1]) "Universe B (hybrid)" else "Universe A (all-ETF)"
cat("Final selected portfolio:", final_name)
## Final selected portfolio: Universe B (hybrid)
port_rets <- final$returns
colnames(port_rets) <- "Strategy"
weights <- final$weights

bench_aligned <- bench_rets[index(port_rets)]
colnames(bench_aligned) <- "Benchmark (SPY)"
comparison <- merge(port_rets, bench_aligned)

3.4 Cumulative Return vs. Benchmark (Final Portfolio)

chart.CumReturns(comparison, wealth.index = TRUE, legend.loc = "topleft",
                 main = paste("Growth of $1:", final_name, "vs. SPY"),
                 colorset = c("steelblue4", "grey55"), lwd = 2)

3.5 Drawdown Analysis

chart.Drawdown(comparison, legend.loc = "bottomleft",
               main = "Drawdown: Strategy vs. SPY",
               colorset = c("steelblue4", "grey55"), lwd = 2)

3.6 Performance Metrics: Sharpe, MDD, Alpha & Beta

ann_ret <- Return.annualized(comparison)
ann_vol <- StdDev.annualized(comparison)
sharpe  <- SharpeRatio.annualized(comparison, Rf = rf_monthly)
mdd     <- maxDrawdown(comparison)

capm_alpha <- CAPM.alpha(port_rets, bench_aligned, Rf = rf_monthly)
capm_beta  <- CAPM.beta(port_rets,  bench_aligned, Rf = rf_monthly)

metrics <- data.frame(
  Metric = c("Annualized Return", "Annualized Volatility",
             "Sharpe Ratio (Rf = 2%)", "Maximum Drawdown",
             "CAPM Alpha (annualized)", "CAPM Beta"),
  Strategy = c(sprintf("%.2f%%", 100 * ann_ret[1, 1]),
               sprintf("%.2f%%", 100 * ann_vol[1, 1]),
               sprintf("%.2f", sharpe[1, 1]),
               sprintf("%.2f%%", -100 * mdd[1, 1]),
               sprintf("%.2f%%", 100 * ((1 + capm_alpha)^12 - 1)),
               sprintf("%.2f", capm_beta)),
  Benchmark = c(sprintf("%.2f%%", 100 * ann_ret[1, 2]),
                sprintf("%.2f%%", 100 * ann_vol[1, 2]),
                sprintf("%.2f", sharpe[1, 2]),
                sprintf("%.2f%%", -100 * mdd[1, 2]),
                "—", "1.00")
)
kable(metrics, caption = "Table 3. Performance summary (monthly data, full backtest period).")
Table 3. Performance summary (monthly data, full backtest period).
Metric Strategy Benchmark
Annualized Return 22.63% 16.00%
Annualized Volatility 15.53% 15.87%
Sharpe Ratio (Rf = 2%) 1.27 0.89
Maximum Drawdown -13.13% -23.93%
CAPM Alpha (annualized) 11.22%
CAPM Beta 0.64 1.00
excess_port  <- port_rets - rf_monthly
excess_bench <- bench_aligned - rf_monthly
capm_fit <- lm(excess_port ~ excess_bench)
summary(capm_fit)
## 
## Call:
## lm(formula = excess_port ~ excess_bench)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -0.074284 -0.022730 -0.002802  0.022902  0.079695 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  0.008904   0.004192   2.124   0.0372 *  
## excess_bench 0.639556   0.089184   7.171 6.51e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.03419 on 69 degrees of freedom
## Multiple R-squared:  0.427,  Adjusted R-squared:  0.4187 
## F-statistic: 51.43 on 1 and 69 DF,  p-value: 6.506e-10

Reading the regression: the intercept is the monthly alpha (multiply by ~12 for an annual approximation); its t-statistic and p-value tell us whether the abnormal return is statistically distinguishable from zero. The slope is the strategy’s beta — a value meaningfully below 1.0 confirms that the cash rule and bond/gold rotations reduce market sensitivity, which is exactly the design intent.

3.7 Month-by-Month Rebalanced Holdings

The table below lists every monthly rebalance: the assets selected, their exact weights, and the cash residual. Each row’s weights are set at that month-end and held through the following month. This is the full, auditable record of the rebalancing procedure described in Section 2.3.

holdings_rows <- lapply(seq_along(index(weights)), function(i) {
  w <- as.numeric(weights[i, ])
  names(w) <- colnames(weights)
  sel <- sort(w[w > 0.0001], decreasing = TRUE)
  txt <- if (length(sel) > 0)
    paste(sprintf("%s %.0f%%", names(sel), 100 * sel), collapse = ", ")
  else ""
  cash <- 1 - sum(sel)
  if (cash > 0.005)
    txt <- paste0(txt, if (nchar(txt) > 0) ", " else "",
                  sprintf("CASH %.0f%%", 100 * cash))
  data.frame(`Rebalance date` = format(index(weights)[i], "%Y-%m"),
             `Number held` = length(sel),
             `Holdings and weights` = txt,
             check.names = FALSE)
})
holdings_tbl <- do.call(rbind, holdings_rows)
kable(holdings_tbl, row.names = FALSE,
      caption = "Table 4. Complete monthly rebalancing record: selected assets, weights, and cash.")
Table 4. Complete monthly rebalancing record: selected assets, weights, and cash.
Rebalance date Number held Holdings and weights
2020-07 5 TLT 33%, GLD 25%, MSFT 15%, NVDA 14%, QQQ 13%
2020-08 5 GLD 32%, MSFT 21%, QQQ 19%, AVGO 15%, NVDA 14%
2020-09 5 EFA 36%, AVGO 23%, QQQ 15%, MSFT 14%, NVDA 12%
2020-10 5 GLD 26%, AVGO 24%, QQQ 20%, MSFT 18%, NVDA 11%
2020-11 5 EFA 24%, AVGO 22%, QQQ 21%, MSFT 20%, NVDA 13%
2020-12 5 XLV 30%, EFA 21%, AVGO 19%, QQQ 19%, NVDA 11%
2021-01 5 AVGO 25%, EFA 25%, QQQ 23%, NVDA 14%, LLY 13%
2021-02 5 QQQ 27%, AVGO 25%, EFA 25%, LLY 13%, XLE 10%
2021-03 5 QQQ 30%, EFA 25%, AVGO 22%, XLE 12%, LLY 11%
2021-04 5 QQQ 31%, EFA 26%, AVGO 21%, XLE 11%, LLY 11%
2021-05 5 MSFT 36%, AVGO 25%, NVDA 17%, XLE 13%, LLY 9%
2021-06 5 QQQ 35%, MSFT 30%, XLE 14%, NVDA 12%, LLY 9%
2021-07 5 QQQ 36%, MSFT 29%, LLY 13%, NVDA 11%, XLE 11%
2021-08 5 XLV 49%, QQQ 21%, MSFT 17%, LLY 7%, NVDA 6%
2021-09 5 XLV 32%, QQQ 25%, MSFT 20%, LLY 13%, NVDA 10%
2021-10 5 AVGO 37%, XLE 20%, MSFT 17%, LLY 16%, NVDA 11%
2021-11 5 AVGO 34%, QQQ 27%, MSFT 16%, LLY 14%, NVDA 9%
2021-12 5 XLV 30%, MSFT 20%, AVGO 20%, LLY 20%, NVDA 10%
2022-01 5 XLE 25%, MSFT 23%, LLY 21%, AVGO 20%, NVDA 11%
2022-02 4 GLD 41%, XLE 18%, AVGO 13%, NVDA 8%, CASH 20%
2022-03 5 GLD 49%, XLE 17%, LLY 14%, AVGO 13%, NVDA 8%
2022-04 4 GLD 41%, XLE 15%, LLY 14%, AVGO 10%, CASH 20%
2022-05 5 GLD 36%, XLV 22%, XLE 17%, LLY 14%, AVGO 10%
2022-06 2 LLY 24%, XLE 16%, CASH 60%
2022-07 3 XLV 28%, LLY 22%, XLE 9%, CASH 40%
2022-08 2 LLY 24%, XLE 16%, CASH 60%
2022-09 1 LLY 20%, CASH 80%
2022-10 3 XLV 28%, LLY 22%, XLE 10%, CASH 40%
2022-11 3 XLV 27%, LLY 23%, XLE 10%, CASH 40%
2022-12 5 XLV 29%, LLY 23%, EFA 19%, AVGO 16%, XLE 14%
2023-01 5 GLD 37%, EFA 21%, AVGO 19%, XLE 16%, NVDA 8%
2023-02 5 GLD 34%, EFA 22%, AVGO 21%, XLE 15%, NVDA 9%
2023-03 5 AVGO 26%, EFA 24%, QQQ 21%, MSFT 19%, NVDA 9%
2023-04 5 GLD 28%, EFA 23%, AVGO 21%, MSFT 19%, NVDA 8%
2023-05 5 MSFT 27%, QQQ 26%, LLY 20%, AVGO 17%, NVDA 10%
2023-06 5 QQQ 32%, MSFT 29%, LLY 15%, AVGO 14%, NVDA 11%
2023-07 5 QQQ 36%, MSFT 24%, LLY 15%, AVGO 13%, NVDA 12%
2023-08 5 QQQ 35%, MSFT 23%, LLY 18%, AVGO 14%, NVDA 12%
2023-09 5 QQQ 34%, XLE 26%, LLY 17%, AVGO 13%, NVDA 10%
2023-10 5 MSFT 32%, QQQ 30%, LLY 16%, AVGO 12%, NVDA 9%
2023-11 5 XLE 28%, MSFT 23%, AVGO 21%, LLY 15%, NVDA 13%
2023-12 5 QQQ 27%, MSFT 25%, LLY 16%, NVDA 16%, AVGO 16%
2024-01 5 QQQ 28%, MSFT 26%, LLY 18%, AVGO 16%, NVDA 12%
2024-02 5 MSFT 27%, QQQ 27%, LLY 21%, AVGO 15%, NVDA 10%
2024-03 5 QQQ 27%, MSFT 27%, LLY 19%, AVGO 17%, NVDA 10%
2024-04 5 EFA 31%, QQQ 25%, LLY 18%, AVGO 16%, NVDA 10%
2024-05 5 GLD 33%, QQQ 28%, LLY 16%, AVGO 14%, NVDA 9%
2024-06 5 QQQ 31%, MSFT 23%, LLY 21%, AVGO 15%, NVDA 10%
2024-07 5 GLD 37%, XLE 28%, AVGO 14%, LLY 13%, NVDA 8%
2024-08 5 GLD 35%, XLV 32%, AVGO 13%, LLY 11%, NVDA 9%
2024-09 5 GLD 48%, QQQ 23%, AVGO 12%, LLY 9%, NVDA 8%
2024-10 5 GLD 36%, TLT 24%, QQQ 23%, AVGO 9%, NVDA 7%
2024-11 5 TLT 26%, QQQ 26%, GLD 25%, NVDA 13%, AVGO 9%
2024-12 4 QQQ 34%, GLD 24%, NVDA 17%, AVGO 5%, CASH 20%
2025-01 5 QQQ 46%, GLD 25%, NVDA 14%, LLY 9%, AVGO 5%
2025-02 5 QQQ 36%, GLD 27%, XLE 17%, NVDA 15%, AVGO 5%
2025-03 2 GLD 22%, XLE 18%, CASH 60%
2025-04 4 EFA 39%, GLD 23%, LLY 13%, AVGO 5%, CASH 20%
2025-05 5 EFA 36%, GLD 26%, QQQ 20%, MSFT 13%, AVGO 5%
2025-06 5 EFA 53%, GLD 24%, MSFT 11%, NVDA 6%, AVGO 6%
2025-07 5 EFA 44%, GLD 28%, MSFT 13%, NVDA 8%, AVGO 7%
2025-08 5 EFA 42%, GLD 28%, MSFT 13%, NVDA 8%, AVGO 8%
2025-09 5 QQQ 35%, GLD 24%, MSFT 16%, AVGO 14%, NVDA 11%
2025-10 5 QQQ 37%, GLD 23%, MSFT 14%, AVGO 13%, NVDA 12%
2025-11 5 XLV 28%, GLD 26%, AVGO 25%, LLY 11%, NVDA 11%
2025-12 5 GLD 32%, XLV 29%, NVDA 14%, AVGO 13%, LLY 12%
2026-01 5 EFA 44%, XLV 19%, GLD 17%, XLE 13%, LLY 7%
2026-02 5 EFA 43%, XLV 20%, GLD 18%, XLE 12%, LLY 7%
2026-03 5 EFA 29%, XLV 23%, XLE 21%, GLD 17%, LLY 10%
2026-04 5 EFA 33%, XLE 25%, GLD 20%, LLY 13%, AVGO 10%
2026-05 5 EFA 31%, NVDA 21%, QQQ 19%, XLE 19%, AVGO 9%
2026-06 5 XLE 24%, NVDA 24%, QQQ 22%, LLY 19%, AVGO 11%

3.8 Allocation History (Visual)

library(ggplot2)
library(tidyr)
library(dplyr)

w_df <- data.frame(Date = index(weights), coredata(weights))
w_df$CASH <- pmax(0, 1 - rowSums(w_df[, colnames(weights)]))
w_long <- pivot_longer(w_df, cols = -Date, names_to = "Asset", values_to = "Weight")

ggplot(w_long, aes(x = Date, y = Weight, fill = Asset)) +
  geom_area(alpha = 0.9) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Monthly Portfolio Allocation Over Time",
       subtitle = "Cash builds up automatically when few assets have positive 6-month momentum",
       y = "Weight", x = NULL) +
  theme_minimal()

cal <- table.CalendarReturns(comparison)
kable(cal[, (ncol(cal) - 1):ncol(cal)],
      caption = "Table 5. Calendar-year returns (%), Strategy vs. SPY.")
Table 5. Calendar-year returns (%), Strategy vs. SPY.
Strategy Benchmark..SPY.
2020 9.1 15.5
2021 47.9 28.7
2022 -10.0 -18.2
2023 41.7 26.2
2024 42.0 24.9
2025 13.9 17.7
2026 0.6 6.7

Part IV: Critical Reflection

4.1 What did the AI provide that traditional analysis might have missed?

Four insights stand out. First, the AI’s strongest contribution was a negative recommendation: it argued against mean-variance optimization, explaining that with short estimation windows, max-Sharpe weights amplify estimation error rather than information — a student following a textbook approach would likely have used MVO precisely because it looks more rigorous. Second, the AI reframed universe construction: instead of picking the 10 “best” assets, it pushed for the 10 most decorrelated trending assets, which is why GLD and TLT sit alongside growth assets — they are there to receive capital when equities fail the momentum filter. Third, when Round-1 results trailed the benchmark, the AI diagnosed the structural cause (sector ETFs cannot overweight the mega-caps that drove 2023–2025) rather than suggesting parameter tweaks — leading to the core–satellite redesign instead of overfitting the lookback window. Fourth, the AI flagged subtle backtest pitfalls a first-time researcher commonly misses: adjusted (dividend-inclusive) prices, lagging weights by one month to avoid look-ahead bias, and starting the data 6 months early so the first signal is not computed on partial data.

4.2 Do the results align with the initial hypothesis?

The hypothesis was that the strategy would deliver (a) a materially smaller maximum drawdown than SPY, (b) a competitive or higher Sharpe ratio, and (c) positive CAPM alpha, while accepting lower raw returns in uninterrupted bull markets.

Round 1 partially confirmed and partially refuted the hypothesis. The defensive predictions held strongly — beta around 0.5 and a maximum drawdown less than half of SPY’s, with the cash rule visibly earning its keep in 2022 (Strategy −9.7% vs SPY −18.2%). But the Sharpe ratio slightly trailed the benchmark, refuting hypothesis (b). The discrepancy had identifiable causes: whipsaw cost (monthly momentum sits partly in cash during V-shaped recoveries, then buys back higher) and an unusually concentrated benchmark regime (2023–2025 returns came from a handful of mega-caps that diversified sector ETFs structurally underweight — the strategy’s 2023 return of 8.1% vs SPY’s 26.2% is the clearest symptom).

Round 2 turned that diagnosis into a design change — the 4-stock quality-momentum satellite — and Table 2 shows whether the revision closed the gap while preserving the drawdown advantage.

Two honest limitations must be stated. First, in-sample selection risk: the satellite stocks were chosen knowing the 2021–2026 period, so part of Universe B’s improvement reflects hindsight; the legitimate, forward-looking claim is the process (quality-momentum screening refreshed periodically), not these four tickers forever. A true out-of-sample test would re-run the screen at each rebalance. Second, statistical power: with roughly 60 monthly observations, even a genuinely positive alpha often fails conventional significance tests (see the regression t-statistic); a non-significant alpha is evidence of a short sample, not necessarily of an absent premium. Transaction costs and taxes, excluded here, would also shave reported returns slightly.

The conclusion is that the strategy behaves as designed — lower beta, far shallower drawdowns, and upside participation restored by the satellite — and that its alpha source (momentum + trend-filtering + low-vol weighting) is theoretically sound but regime-dependent: it is built to win by losing less in bad markets, and the iterative universe revision shows how a quantitative investor responds when one leg of the hypothesis fails.


All data: Yahoo Finance via quantmod. Analysis: R (PerformanceAnalytics, xts, ggplot2). AI assistance (Claude) was used for strategy design critique, universe construction, fundamental stock screening, and code review, as documented in Section 1.4.