1 Part I: Investment Thesis and Logic

1.1 Strategy Overview

This portfolio applies a Quality-Momentum (QMO) factor strategy within a globally diversified framework. Rather than relying on market-cap-weighted exposure, the strategy systematically tilts toward companies that demonstrate measurable financial strength and exhibit positive price persistence — two characteristics that academic and practitioner research associates with superior risk-adjusted returns over time.

The strategy is executed primarily through factor ETFs and a small number of individual securities, reflecting both the theoretical underpinning of systematic factor investing and the practical constraints of a student-managed portfolio.


1.2 Source of Alpha: Economic and Behavioral Rationale

1.2.1 The Quality Factor

Quality investing selects companies based on measures of financial health: high return on equity, low financial leverage, stable earnings growth, and strong free cash flow generation. The theoretical case for a quality premium rests on two complementary explanations.

From a risk perspective, high-quality firms tend to be more resilient during economic contractions. They hold less debt and generate more consistent cash flows, which reduces the probability of financial distress. If investors rationally price this resilience, quality stocks should trade at a premium — but the evidence suggests they often do not.

From a behavioral perspective, investors systematically undervalue quality. Asness, Frazzini, and Pedersen (2019) demonstrated that quality companies trade at lower valuations than their fundamentals justify, creating a persistent mispricing that patient capital can exploit. This mispricing is partly attributable to overconfidence in short-term earnings forecasts and inattention to balance sheet durability. When earnings surprises eventually confirm quality, the stock re-rates, generating abnormal returns.

1.2.2 The Momentum Factor

Momentum investing selects stocks based on recent price performance, typically measured over a 12-month window excluding the most recent month. The foundation of this strategy lies in behavioral inefficiency: markets are slow to fully incorporate new information.

Jegadeesh and Titman (1993) documented that stocks in the top decile of 12-month returns continued to outperform stocks in the bottom decile by approximately 1% per month over the subsequent 6–12 months — a finding replicated across most developed and many emerging markets. The mechanism is driven by underreaction: investors initially discount positive news, and prices drift upward over several months as the information is progressively absorbed. Institutional herding and analyst slow revision compound this effect.

1.2.3 Why the Combination Matters

Quality and momentum have a historically low to negative correlation with each other. Momentum strategies suffer during sharp market reversals (momentum crashes), while quality strategies tend to lag in strong risk-on rallies when investors chase lower-quality, higher-beta names. Combining both factors produces a portfolio that draws on two distinct sources of mispricing while partially hedging the idiosyncratic failure modes of each.

The combined strategy is designed to capture alpha through behavioral mispricing — specifically, the market’s tendency to underreact to durable earnings quality and to underweight the persistence of recent price trends.


1.3 Macroeconomic Context and AI-Assisted Analysis

AI was used to assess the current macroeconomic environment and its implications for strategy selection. The following themes were identified through structured AI interaction:

Interest Rate Environment: In a higher-for-longer interest rate environment, companies with strong profitability and low debt (the quality factor) face lower refinancing risk and less earnings dilution from interest expense. Speculative, high-leverage companies — which dominate low-quality indices — are disproportionately penalized. This macroeconomic backdrop structurally favors the quality tilt in this portfolio.

AI and Semiconductor Cycle: The portfolio’s holdings include ASML Holding NV and QUAL (which carries significant weight in technology-linked quality names). AI-related capital expenditure has driven a structural upswing in semiconductor equipment demand. ASML holds a near-monopoly on extreme ultraviolet (EUV) lithography systems, creating durable pricing power that aligns with the quality criteria of stable earnings and high margins.

Japan Corporate Governance Reform: The Tokyo Stock Exchange’s campaign to improve corporate governance — targeting companies trading below book value and pressuring them to improve capital efficiency — is creating a structural re-rating opportunity in Japanese equities. EWJ provides direct exposure to this theme, and the governance improvements are gradually improving the return-on-equity profile of Japanese corporates, making them increasingly compatible with quality screening criteria.

Emerging Market Diversification: VWO provides exposure to long-term demographic and consumption growth trends in emerging markets, offering diversification away from US and developed-market concentration, at the cost of higher political and currency risk.


1.4 AI Collaboration Process

This section documents the actual prompts used with the AI system (Claude) throughout the project, and explains how each interaction shaped the final portfolio and analysis.

1.4.1 Prompt 1 — Strategy Ideation

“Act as a quantitative portfolio manager with expertise in factor investing. I am building a globally diversified portfolio for an undergraduate finance project. The portfolio can hold a maximum of 10 ETFs or stocks. Recommend a factor-based strategy that: (1) has strong academic support, (2) is implementable using publicly traded ETFs, (3) is appropriate for a 3-year backtesting window, and (4) provides a clear theoretical explanation for why it should generate alpha above the ACWI benchmark.”

AI Contribution: The AI recommended a Quality-Momentum hybrid and explained the behavioral finance rationale (underreaction and mispricing) in detail. It also identified that combining both factors reduces exposure to each factor’s individual failure mode. This directly shaped the strategy selection in Part I.

1.4.2 Prompt 2 — Asset Selection and Global Diversification

“I want to construct a 10-asset globally diversified portfolio using iShares and Vanguard factor ETFs. The portfolio should have exposure to: US quality, US momentum, international developed momentum, Japan, emerging markets, healthcare, and at least one defensive asset. Recommend specific ETF tickers and justify each selection in terms of factor exposure, expense ratio, and liquidity.”

AI Contribution: The AI recommended the core ETF list (QUAL, MTUM, IMTM, EWJ, VWO, IHI, GLD) and flagged that IBIT’s launch in January 2024 would truncate any backtest attempting 3-year coverage. It suggested replacing IBIT with IQLT (iShares MSCI World Quality Factor ETF) as a more academically defensible alternative with a longer history and stronger alignment with the strategy’s quality thesis.

1.4.3 Prompt 3 — Concentration Risk Identification

“Here is my proposed ETF portfolio: QUAL (18%), IMTM (15%), MTUM (15%), MSCI (12%), ASML (12%), EWJ (8%), VWO (8%), IHI (5%), IBIT (4%), GLD (3%). Identify all hidden concentration risks, factor overlaps, and diversification weaknesses. Be critical and specific.”

AI Contribution: The AI identified that QUAL and MTUM both hold Apple, Microsoft, and Nvidia as top positions, creating a hidden concentration in US large-cap technology that is not apparent from the ticker list alone. It estimated the combined overlap at approximately 40–50% of the underlying holdings. This insight led to the decision to reduce MTUM’s weight and introduce IQLT as an international quality complement.

1.4.4 Prompt 4 — Optimization Code Generation

“Write a complete R script using PortfolioAnalytics and PerformanceAnalytics to: (1) download 3 years of daily adjusted close prices for a list of 10 tickers using tidyquant, (2) optimize portfolio weights using Maximum Sharpe Ratio with constraints of fully invested, long-only, max weight 20%, min weight 2%, (3) backtest the optimized portfolio against the ACWI benchmark, (4) calculate cumulative return, Sharpe ratio, max drawdown, alpha, and beta, and (5) produce ggplot2 charts for cumulative returns, drawdowns, and portfolio weights.”

AI Contribution: The AI generated the initial R code structure including data download, return calculation, PortfolioAnalytics specification, and PerformanceAnalytics metrics. Several bugs were identified in subsequent debugging prompts, particularly around xts alignment when IBIT’s shorter history caused NA propagation — a problem solved by switching to IQLT.

1.4.5 Prompt 5 — Macro Analysis

“Assess the current macroeconomic environment (as of early 2024 onward) and explain which macro conditions favor a Quality-Momentum strategy versus when they would hurt it. Specifically discuss: interest rates, AI investment cycle, Japanese corporate governance reform, and emerging market risk.”

AI Contribution: This prompt produced the macroeconomic context summarized in Section 1.3 above. The AI’s analysis directly informed the benchmark justification and the selection of EWJ and VWO as diversification instruments.

1.4.6 Prompt 6 — Debugging

“My R backtest is producing NA values in the summary table for cumulative return and max drawdown. The metrics print correctly in earlier code chunks but fail to populate the kable table. Here is the relevant code: [code pasted]. Identify the bug and fix it.”

AI Contribution: The AI identified that the issue arose from storing CAPM results and PerformanceAnalytics outputs in incompatible object classes before coercing to a data frame. It provided a corrected version that explicitly extracts numeric values using as.numeric() before table construction — the approach used in Part III of this report.


2 Part II: Portfolio Construction

2.1 Final Portfolio Holdings

The final portfolio replaces IBIT (Bitcoin ETF, launched January 2024) with IQLT (iShares MSCI World Quality Factor ETF), which provides international developed-market quality exposure, has a trading history going back to 2015, and strengthens the portfolio’s alignment with the quality-momentum thesis. This change also resolves the historical data truncation that limited the previous backtest to approximately two years.

# ── Asset universe ──────────────────────────────────────────────────────────
tickers <- c("QUAL", "IMTM", "MTUM", "MSCI", "ASML",
             "EWJ",  "VWO",  "IHI",  "IQLT", "GLD")

benchmark_ticker <- "ACWI"

# Initial target weights (equal-risk starting point; optimizer will refine)
initial_weights <- c(0.18, 0.15, 0.13, 0.10, 0.10,
                     0.08, 0.08, 0.07, 0.07, 0.04)

# Labels for display
asset_labels <- c(
  "QUAL"  = "iShares MSCI USA Quality Factor ETF",
  "IMTM"  = "iShares MSCI Intl Developed Momentum ETF",
  "MTUM"  = "iShares MSCI USA Momentum Factor ETF",
  "MSCI"  = "MSCI Inc. (Individual Stock)",
  "ASML"  = "ASML Holding NV (Individual Stock)",
  "EWJ"   = "iShares MSCI Japan ETF",
  "VWO"   = "Vanguard FTSE Emerging Markets ETF",
  "IHI"   = "iShares U.S. Medical Devices ETF",
  "IQLT"  = "iShares MSCI World Quality Factor ETF",
  "GLD"   = "SPDR Gold Shares ETF"
)

# Portfolio holdings table
holdings_df <- tibble(
  Ticker   = tickers,
  Name     = asset_labels[tickers],
  `Initial Weight (%)` = initial_weights * 100,
  Role     = c(
    "Core quality factor — US large-cap",
    "Momentum factor — international developed",
    "Momentum factor — US large-cap",
    "High-quality individual stock: financial data/indices",
    "High-quality individual stock: semiconductor equipment",
    "Geographic diversification — Japan governance reform",
    "Geographic diversification — emerging markets growth",
    "Sector diversification — healthcare/medical devices",
    "Quality factor — international developed (replaces IBIT)",
    "Defensive diversifier — inflation hedge"
  ),
  `Primary Risk` = c(
    "Tech concentration, factor crowding",
    "Currency risk, international volatility",
    "Momentum crash during reversals",
    "High valuation, regulatory risk",
    "Geopolitical/export restriction risk",
    "Yen depreciation, interest rate sensitivity",
    "Emerging market political and currency risk",
    "Sector concentration, pricing pressure",
    "Factor overlap with QUAL, currency risk",
    "No income, underperforms in strong equity rallies"
  )
)

kable(holdings_df, caption = "Table 1: Portfolio Holdings, Roles, and Risk Summary",
      align = c("l","l","r","l","l")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 12)
Table 1: Portfolio Holdings, Roles, and Risk Summary
Ticker Name Initial Weight (%) Role Primary Risk
QUAL iShares MSCI USA Quality Factor ETF 18 Core quality factor — US large-cap Tech concentration, factor crowding
IMTM iShares MSCI Intl Developed Momentum ETF 15 Momentum factor — international developed Currency risk, international volatility
MTUM iShares MSCI USA Momentum Factor ETF 13 Momentum factor — US large-cap Momentum crash during reversals
MSCI MSCI Inc. (Individual Stock) 10 High-quality individual stock: financial data/indices High valuation, regulatory risk
ASML ASML Holding NV (Individual Stock) 10 High-quality individual stock: semiconductor equipment Geopolitical/export restriction risk
EWJ iShares MSCI Japan ETF 8 Geographic diversification — Japan governance reform Yen depreciation, interest rate sensitivity
VWO Vanguard FTSE Emerging Markets ETF 8 Geographic diversification — emerging markets growth Emerging market political and currency risk
IHI iShares U.S. Medical Devices ETF 7 Sector diversification — healthcare/medical devices Sector concentration, pricing pressure
IQLT iShares MSCI World Quality Factor ETF 7 Quality factor — international developed (replaces IBIT) Factor overlap with QUAL, currency risk
GLD SPDR Gold Shares ETF 4 Defensive diversifier — inflation hedge No income, underperforms in strong equity rallies

2.2 Benchmark Selection: ACWI vs SPY

The primary benchmark is ACWI (iShares MSCI All Country World ETF). Using SPY (S&P 500) as the benchmark for this portfolio would introduce a geographic attribution problem: SPY represents approximately 500 large US companies, while this portfolio allocates roughly 40–45% of weight outside the United States through IMTM, EWJ, VWO, IQLT, and ASML. Any outperformance vs. SPY during a period of international equity outperformance would be misattributed to strategy skill rather than geographic diversification.

ACWI covers over 2,900 companies across 47 developed and emerging market countries, making it a genuinely neutral starting point for a globally diversified portfolio. Measuring alpha and beta against ACWI ensures that only the strategy-specific factor tilts — not geographic allocation decisions — appear as excess returns.


2.3 Data Download and Preparation

# ── Date range: 3 full years of daily data ───────────────────────────────────
end_date   <- Sys.Date()
start_date <- end_date - years(3) - days(10)   # slight buffer for weekends/holidays

all_tickers <- c(tickers, benchmark_ticker)

# Download adjusted close prices via tidyquant
raw_prices <- tq_get(
  all_tickers,
  from = start_date,
  to   = end_date,
  get  = "stock.prices"
)

# Pivot to wide format
prices_wide <- raw_prices %>%
  select(date, symbol, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date) %>%
  drop_na()   # Remove any days with missing data across ALL assets

cat("Date range:", format(min(prices_wide$date)), "to", format(max(prices_wide$date)), "\n")
## Date range: 2023-05-16 to 2026-05-22
cat("Trading days:", nrow(prices_wide), "\n")
## Trading days: 758
cat("Assets downloaded:", ncol(prices_wide) - 1, "\n")
## Assets downloaded: 11
# ── Data completeness table ───────────────────────────────────────────────────
completeness <- raw_prices %>%
  group_by(symbol) %>%
  summarise(
    start     = min(date),
    end       = max(date),
    n_obs     = n(),
    pct_avail = round(n() / as.numeric(end_date - start_date) * 100, 1)
  ) %>%
  arrange(symbol)

kable(completeness,
      col.names = c("Ticker", "Start Date", "End Date", "Observations", "% Coverage"),
      caption   = "Table 2: Data Completeness Summary") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Table 2: Data Completeness Summary
Ticker Start Date End Date Observations % Coverage
ACWI 2023-05-16 2026-05-22 758 68.5
ASML 2023-05-16 2026-05-22 758 68.5
EWJ 2023-05-16 2026-05-22 758 68.5
GLD 2023-05-16 2026-05-22 758 68.5
IHI 2023-05-16 2026-05-22 758 68.5
IMTM 2023-05-16 2026-05-22 758 68.5
IQLT 2023-05-16 2026-05-22 758 68.5
MSCI 2023-05-16 2026-05-22 758 68.5
MTUM 2023-05-16 2026-05-22 758 68.5
QUAL 2023-05-16 2026-05-22 758 68.5
VWO 2023-05-16 2026-05-22 758 68.5
# ── Convert to xts and calculate daily log returns ────────────────────────────
prices_xts <- prices_wide %>%
  column_to_rownames("date") %>%
  as.xts()

# Daily arithmetic returns (required by PortfolioAnalytics)
returns_all <- Return.calculate(prices_xts, method = "discrete")
returns_all <- returns_all[-1, ]   # Drop first NA row

# Separate portfolio assets from benchmark
portfolio_returns_raw <- returns_all[, tickers]
benchmark_returns_raw <- returns_all[, benchmark_ticker]

# Drop any remaining rows with NA (belt-and-suspenders)
complete_idx           <- complete.cases(portfolio_returns_raw)
portfolio_returns_raw  <- portfolio_returns_raw[complete_idx, ]
benchmark_returns_raw  <- benchmark_returns_raw[complete_idx, ]

cat("Clean daily returns available:", nrow(portfolio_returns_raw), "trading days\n")
## Clean daily returns available: 757 trading days

3 Part III: Portfolio Optimization

3.1 Optimization Objective and Constraints

The portfolio is optimized for the Maximum Sharpe Ratio (MSR), which identifies the set of weights that maximizes the excess return per unit of total risk. This is equivalent to finding the tangency portfolio on the mean-variance efficient frontier — the single portfolio that rational investors with any positive risk aversion would prefer to hold in combination with a risk-free asset.

Why Maximum Sharpe Ratio? Unlike minimum-variance optimization, which ignores expected returns entirely, MSR balances the return and risk objectives simultaneously. For a strategy thesis claiming to deliver alpha through factor exposure, MSR is the most internally consistent objective: it directly maximizes the risk-adjusted metric used to evaluate the strategy’s success.

Constraints applied: - Fully invested: weights sum to 1.0, ensuring no uninvested cash drag. - Long-only: no short positions, appropriate for a buy-and-hold undergraduate portfolio. - Maximum weight 20%: prevents any single asset from dominating the portfolio, which is especially important given the QUAL/MTUM overlap in US technology. - Minimum weight 2%: ensures all selected assets maintain a meaningful allocation. Without a floor, unconstrained optimization frequently assigns near-zero weights to assets whose inclusion was theoretically motivated, effectively ignoring the diversification rationale.

Limitations of the optimization approach: - Mean-variance optimization is highly sensitive to input estimates. Small changes in expected return assumptions can produce large swings in optimized weights, a problem known as estimation error amplification. - The backtest uses historical means and covariances estimated over the same data used to evaluate performance, which introduces an in-sample optimization bias — the optimizer “knows” which assets performed well. - In practice, the optimization would use a rolling window with an out-of-sample evaluation period to mitigate this bias.

# ── Portfolio specification ───────────────────────────────────────────────────
port_spec <- portfolio.spec(assets = tickers)

# Constraints
port_spec <- add.constraint(port_spec, type = "full_investment")
port_spec <- add.constraint(port_spec, type = "long_only")
port_spec <- add.constraint(port_spec, type = "box",
                            min = 0.02, max = 0.20)

# Objectives: maximize return-to-risk ratio (Max Sharpe)
port_spec <- add.objective(port_spec, type = "return", name = "mean")
port_spec <- add.objective(port_spec, type = "risk",   name = "StdDev")

# Run optimization (ROI solver)
opt_result <- optimize.portfolio(
  R                = portfolio_returns_raw,
  portfolio        = port_spec,
  optimize_method  = "ROI",
  maxSR            = TRUE,
  trace            = FALSE
)

opt_weights <- extractWeights(opt_result)

# ── Display optimized weights ─────────────────────────────────────────────────
weights_df <- tibble(
  Ticker = names(opt_weights),
  Name   = asset_labels[names(opt_weights)],
  `Optimized Weight (%)` = round(opt_weights * 100, 2)
) %>%
  arrange(desc(`Optimized Weight (%)`))

kable(weights_df,
      caption = "Table 3: Optimized Portfolio Weights (Maximum Sharpe Ratio)",
      align   = c("l","l","r")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
  row_spec(which(weights_df$`Optimized Weight (%)` >= 18), bold = TRUE)
Table 3: Optimized Portfolio Weights (Maximum Sharpe Ratio)
Ticker Name Optimized Weight (%)
QUAL iShares MSCI USA Quality Factor ETF 20.00
MTUM iShares MSCI USA Momentum Factor ETF 20.00
GLD SPDR Gold Shares ETF 20.00
IMTM iShares MSCI Intl Developed Momentum ETF 14.08
VWO Vanguard FTSE Emerging Markets ETF 13.92
MSCI MSCI Inc. (Individual Stock) 4.01
ASML ASML Holding NV (Individual Stock) 2.00
EWJ iShares MSCI Japan ETF 2.00
IHI iShares U.S. Medical Devices ETF 2.00
IQLT iShares MSCI World Quality Factor ETF 2.00
weights_plot_df <- weights_df %>%
  mutate(Ticker = factor(Ticker, levels = Ticker))  # preserve sort order

ggplot(weights_plot_df, aes(x = Ticker, y = `Optimized Weight (%)`, fill = `Optimized Weight (%)`)) +
  geom_col(width = 0.65, color = "white") +
  geom_text(aes(label = paste0(`Optimized Weight (%)`, "%")),
            hjust = -0.15, size = 3.5, fontface = "bold") +
  scale_fill_gradient(low = "#cce5ff", high = "#1a6eb5", guide = "none") +
  scale_y_continuous(limits = c(0, 25), labels = function(x) paste0(x, "%")) +
  coord_flip() +
  labs(
    title    = "Optimized Portfolio Weights — Quality-Momentum Global Portfolio",
    subtitle = "Maximum Sharpe Ratio | Constraints: 2% min, 20% max",
    x        = NULL,
    y        = "Allocation (%)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold", size = 13),
    plot.subtitle = element_text(color = "grey40", size = 10),
    panel.grid.major.y = element_blank(),
    axis.text.y   = element_text(face = "bold")
  )
Figure 1: Optimized Portfolio Weights — Maximum Sharpe Ratio

Figure 1: Optimized Portfolio Weights — Maximum Sharpe Ratio


4 Part IV: Backtesting and Performance Analysis

4.1 Portfolio Return Calculation

# ── Apply optimized weights to compute portfolio daily returns ─────────────────
portfolio_daily <- Return.portfolio(
  R         = portfolio_returns_raw,
  weights   = opt_weights,
  rebalance_on = "quarters"   # Quarterly rebalancing assumption
)
colnames(portfolio_daily) <- "Portfolio"

# Align benchmark to same dates
benchmark_daily <- benchmark_returns_raw[index(portfolio_daily)]
colnames(benchmark_daily) <- "ACWI"

# Combined return series
combined_returns <- merge.xts(portfolio_daily, benchmark_daily)
combined_returns <- na.omit(combined_returns)

4.2 Performance Metrics

# ── Risk-free rate (annualized 4.5%, converted to daily) ──────────────────────
rf_annual <- 0.045
rf_daily  <- (1 + rf_annual)^(1/252) - 1

# ── CAPM regression ───────────────────────────────────────────────────────────
excess_port  <- combined_returns[, "Portfolio"] - rf_daily
excess_bench <- combined_returns[, "ACWI"]      - rf_daily

capm_df <- data.frame(
  y = as.numeric(excess_port),
  x = as.numeric(excess_bench)
)
capm_fit <- lm(y ~ x, data = capm_df)
alpha_ann <- coef(capm_fit)[1] * 252
beta_val  <- coef(capm_fit)[2]

# ── Core metrics ──────────────────────────────────────────────────────────────
cum_port  <- as.numeric(Return.cumulative(combined_returns[, "Portfolio"]))
cum_bench <- as.numeric(Return.cumulative(combined_returns[, "ACWI"]))

ann_port  <- as.numeric(Return.annualized(combined_returns[, "Portfolio"],  scale = 252))
ann_bench <- as.numeric(Return.annualized(combined_returns[, "ACWI"],       scale = 252))

vol_port  <- as.numeric(StdDev.annualized(combined_returns[, "Portfolio"],  scale = 252))
vol_bench <- as.numeric(StdDev.annualized(combined_returns[, "ACWI"],       scale = 252))

sr_port   <- as.numeric(SharpeRatio.annualized(combined_returns[, "Portfolio"],
                          Rf = rf_daily, scale = 252))
sr_bench  <- as.numeric(SharpeRatio.annualized(combined_returns[, "ACWI"],
                          Rf = rf_daily, scale = 252))

mdd_port  <- as.numeric(maxDrawdown(combined_returns[, "Portfolio"]))
mdd_bench <- as.numeric(maxDrawdown(combined_returns[, "ACWI"]))

corr_val  <- as.numeric(cor(combined_returns[, "Portfolio"], combined_returns[, "ACWI"]))

# ── Summary table — all values explicitly coerced to numeric ──────────────────
perf_table <- tibble(
  Metric     = c(
    "Cumulative Return",
    "Annualized Return",
    "Annualized Volatility",
    "Sharpe Ratio (Rf = 4.5%)",
    "Maximum Drawdown",
    "CAPM Alpha (annualized)",
    "Beta vs ACWI",
    "Correlation with ACWI"
  ),
  Portfolio  = c(
    paste0(round(cum_port  * 100, 2), "%"),
    paste0(round(ann_port  * 100, 2), "%"),
    paste0(round(vol_port  * 100, 2), "%"),
    round(sr_port,  4),
    paste0("-", round(mdd_port  * 100, 2), "%"),
    paste0(round(alpha_ann * 100, 4), "%"),
    round(beta_val, 4),
    round(corr_val, 4)
  ),
  `ACWI Benchmark` = c(
    paste0(round(cum_bench  * 100, 2), "%"),
    paste0(round(ann_bench  * 100, 2), "%"),
    paste0(round(vol_bench  * 100, 2), "%"),
    round(sr_bench, 4),
    paste0("-", round(mdd_bench  * 100, 2), "%"),
    "—",
    "1.0000",
    "1.0000"
  )
)

kable(perf_table,
      caption = "Table 4: Portfolio Performance Summary vs. ACWI Benchmark",
      align   = c("l","r","r")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
  row_spec(0, bold = TRUE, background = "#1a6eb5", color = "white") %>%
  row_spec(c(1,4,5), background = "#f0f7ff")
Table 4: Portfolio Performance Summary vs. ACWI Benchmark
Metric Portfolio ACWI Benchmark
Cumulative Return 91.7% 79.62%
Annualized Return 24.19% 21.53%
Annualized Volatility 13.82% 14.32%
Sharpe Ratio (Rf = 4.5%) 1.3184 1.1256
Maximum Drawdown -12.64% -16.55%
CAPM Alpha (annualized) 3.8111%
Beta vs ACWI 0.8939 1.0000
Correlation with ACWI 0.9264 1.0000

4.3 Cumulative Return Chart

# ── Build tidy data for ggplot ─────────────────────────────────────────────────
cum_df <- combined_returns %>%
  as.data.frame() %>%
  rownames_to_column("date") %>%
  mutate(date = as.Date(date)) %>%
  mutate(
    cum_port  = cumprod(1 + Portfolio) - 1,
    cum_bench = cumprod(1 + ACWI)      - 1
  ) %>%
  pivot_longer(cols = c(cum_port, cum_bench),
               names_to = "Series", values_to = "CumReturn") %>%
  mutate(Series = recode(Series,
                         cum_port  = "Quality-Momentum Portfolio",
                         cum_bench = "ACWI Benchmark"))

ggplot(cum_df, aes(x = date, y = CumReturn, color = Series, linetype = Series)) +
  geom_line(linewidth = 0.9) +
  scale_color_manual(values = c("Quality-Momentum Portfolio" = "#1a6eb5",
                                "ACWI Benchmark"            = "#c0392b")) +
  scale_linetype_manual(values = c("Quality-Momentum Portfolio" = "solid",
                                   "ACWI Benchmark"            = "dashed")) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  geom_hline(yintercept = 0, linetype = "dotted", color = "grey60") +
  labs(
    title    = "Cumulative Returns: Quality-Momentum Portfolio vs. ACWI Benchmark",
    subtitle = paste0("3-Year Backtest | Quarterly Rebalancing | Rf = 4.5%"),
    x        = NULL,
    y        = "Cumulative Return",
    color    = NULL,
    linetype = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title      = element_text(face = "bold", size = 13),
    plot.subtitle   = element_text(color = "grey40", size = 10),
    legend.position = "bottom",
    axis.text.x     = element_text(angle = 30, hjust = 1)
  )
Figure 2: Cumulative Returns — Portfolio vs. ACWI Benchmark

Figure 2: Cumulative Returns — Portfolio vs. ACWI Benchmark

4.4 Drawdown Chart

# ── Calculate drawdown series ──────────────────────────────────────────────────
dd_port  <- Drawdowns(combined_returns[, "Portfolio"])
dd_bench <- Drawdowns(combined_returns[, "ACWI"])

dd_df <- data.frame(
  date        = as.Date(index(dd_port)),
  Portfolio   = as.numeric(dd_port),
  ACWI        = as.numeric(dd_bench)
) %>%
  pivot_longer(cols = c(Portfolio, ACWI),
               names_to = "Series", values_to = "Drawdown") %>%
  mutate(Series = recode(Series,
                         Portfolio = "Quality-Momentum Portfolio",
                         ACWI      = "ACWI Benchmark"))

ggplot(dd_df, aes(x = date, y = Drawdown, fill = Series, color = Series)) +
  geom_area(alpha = 0.25, position = "identity") +
  geom_line(linewidth = 0.6) +
  scale_fill_manual(values  = c("Quality-Momentum Portfolio" = "#1a6eb5",
                                "ACWI Benchmark"            = "#c0392b")) +
  scale_color_manual(values = c("Quality-Momentum Portfolio" = "#1a6eb5",
                                "ACWI Benchmark"            = "#c0392b")) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  geom_hline(yintercept = 0, color = "grey30") +
  labs(
    title    = "Drawdown Comparison: Portfolio vs. ACWI Benchmark",
    subtitle = "Percentage decline from rolling peak value",
    x        = NULL,
    y        = "Drawdown (%)",
    fill     = NULL,
    color    = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title      = element_text(face = "bold", size = 13),
    plot.subtitle   = element_text(color = "grey40", size = 10),
    legend.position = "bottom",
    axis.text.x     = element_text(angle = 30, hjust = 1)
  )
Figure 3: Drawdown Comparison — Portfolio vs. ACWI Benchmark

Figure 3: Drawdown Comparison — Portfolio vs. ACWI Benchmark

4.5 Rolling Volatility

# ── 63-day rolling volatility (approximately 3 months) ────────────────────────
roll_vol_port  <- rollapply(combined_returns[, "Portfolio"],
                            width = 63, FUN = sd, fill = NA) * sqrt(252)
roll_vol_bench <- rollapply(combined_returns[, "ACWI"],
                            width = 63, FUN = sd, fill = NA) * sqrt(252)

vol_df <- data.frame(
  date      = as.Date(index(roll_vol_port)),
  Portfolio = as.numeric(roll_vol_port),
  ACWI      = as.numeric(roll_vol_bench)
) %>%
  na.omit() %>%
  pivot_longer(cols = c(Portfolio, ACWI),
               names_to = "Series", values_to = "Volatility") %>%
  mutate(Series = recode(Series,
                         Portfolio = "Quality-Momentum Portfolio",
                         ACWI      = "ACWI Benchmark"))

ggplot(vol_df, aes(x = date, y = Volatility, color = Series, linetype = Series)) +
  geom_line(linewidth = 0.85) +
  scale_color_manual(values = c("Quality-Momentum Portfolio" = "#1a6eb5",
                                "ACWI Benchmark"            = "#c0392b")) +
  scale_linetype_manual(values = c("Quality-Momentum Portfolio" = "solid",
                                   "ACWI Benchmark"            = "dashed")) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(
    title    = "Rolling 63-Day Annualized Volatility",
    subtitle = "3-month rolling window",
    x        = NULL,
    y        = "Annualized Volatility",
    color    = NULL,
    linetype = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title      = element_text(face = "bold", size = 13),
    legend.position = "bottom",
    axis.text.x     = element_text(angle = 30, hjust = 1)
  )
Figure 4: Rolling 63-Day Annualized Volatility

Figure 4: Rolling 63-Day Annualized Volatility

4.6 Rolling Sharpe Ratio

# ── 126-day (6-month) rolling Sharpe ratio ────────────────────────────────────
roll_sharpe_fn <- function(x) {
  mean(x - rf_daily) / sd(x) * sqrt(252)
}

roll_sr_port  <- rollapply(combined_returns[, "Portfolio"],
                           width = 126, FUN = roll_sharpe_fn, fill = NA)
roll_sr_bench <- rollapply(combined_returns[, "ACWI"],
                           width = 126, FUN = roll_sharpe_fn, fill = NA)

sr_df <- data.frame(
  date      = as.Date(index(roll_sr_port)),
  Portfolio = as.numeric(roll_sr_port),
  ACWI      = as.numeric(roll_sr_bench)
) %>%
  na.omit() %>%
  pivot_longer(cols = c(Portfolio, ACWI),
               names_to = "Series", values_to = "SharpeRatio") %>%
  mutate(Series = recode(Series,
                         Portfolio = "Quality-Momentum Portfolio",
                         ACWI      = "ACWI Benchmark"))

ggplot(sr_df, aes(x = date, y = SharpeRatio, color = Series, linetype = Series)) +
  geom_line(linewidth = 0.85) +
  geom_hline(yintercept = 0, linetype = "dotted", color = "grey50") +
  geom_hline(yintercept = 1, linetype = "dashed", color = "grey70", alpha = 0.6) +
  scale_color_manual(values = c("Quality-Momentum Portfolio" = "#1a6eb5",
                                "ACWI Benchmark"            = "#c0392b")) +
  scale_linetype_manual(values = c("Quality-Momentum Portfolio" = "solid",
                                   "ACWI Benchmark"            = "dashed")) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(
    title    = "Rolling 126-Day Annualized Sharpe Ratio",
    subtitle = "6-month rolling window | Rf = 4.5% annualized",
    x        = NULL,
    y        = "Sharpe Ratio",
    color    = NULL,
    linetype = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title      = element_text(face = "bold", size = 13),
    legend.position = "bottom",
    axis.text.x     = element_text(angle = 30, hjust = 1)
  )
Figure 5: Rolling 126-Day Annualized Sharpe Ratio

Figure 5: Rolling 126-Day Annualized Sharpe Ratio

4.7 Correlation Table

# ── Pairwise correlation of all assets ────────────────────────────────────────
cor_matrix <- cor(as.data.frame(portfolio_returns_raw), use = "complete.obs")
cor_matrix_rounded <- round(cor_matrix, 3)

kable(cor_matrix_rounded,
      caption = "Table 5: Pairwise Correlation Matrix — Daily Returns",
      align   = rep("r", ncol(cor_matrix_rounded))) %>%
  kable_styling(bootstrap_options = c("striped","condensed","hover"),
                full_width = TRUE, font_size = 10) %>%
  column_spec(1, bold = TRUE)
Table 5: Pairwise Correlation Matrix — Daily Returns
QUAL IMTM MTUM MSCI ASML EWJ VWO IHI IQLT GLD
QUAL 1.000 0.742 0.879 0.441 0.651 0.672 0.671 0.630 0.768 0.147
IMTM 0.742 1.000 0.740 0.303 0.603 0.852 0.758 0.530 0.926 0.383
MTUM 0.879 0.740 1.000 0.328 0.671 0.633 0.624 0.504 0.691 0.186
MSCI 0.441 0.303 0.328 1.000 0.189 0.272 0.253 0.343 0.328 0.012
ASML 0.651 0.603 0.671 0.189 1.000 0.528 0.566 0.340 0.648 0.146
EWJ 0.672 0.852 0.633 0.272 0.528 1.000 0.660 0.468 0.800 0.284
VWO 0.671 0.758 0.624 0.253 0.566 0.660 1.000 0.433 0.809 0.365
IHI 0.630 0.530 0.504 0.343 0.340 0.468 0.433 1.000 0.566 0.144
IQLT 0.768 0.926 0.691 0.328 0.648 0.800 0.809 0.566 1.000 0.360
GLD 0.147 0.383 0.186 0.012 0.146 0.284 0.365 0.144 0.360 1.000

4.8 Individual Asset Contribution Analysis

# ── Estimate contribution to portfolio return ─────────────────────────────────
asset_cumret <- sapply(tickers, function(t) {
  as.numeric(Return.cumulative(portfolio_returns_raw[, t]))
})
asset_annvol <- sapply(tickers, function(t) {
  as.numeric(StdDev.annualized(portfolio_returns_raw[, t], scale = 252))
})
asset_sr <- sapply(tickers, function(t) {
  as.numeric(SharpeRatio.annualized(portfolio_returns_raw[, t],
                                    Rf = rf_daily, scale = 252))
})

contrib_df <- tibble(
  Ticker              = tickers,
  `Weight (%)`        = round(opt_weights * 100, 2),
  `Cum. Return (%)`   = round(asset_cumret * 100, 2),
  `Ann. Volatility (%)` = round(asset_annvol * 100, 2),
  `Sharpe Ratio`      = round(asset_sr, 3)
) %>%
  arrange(desc(`Weight (%)`))

kable(contrib_df,
      caption = "Table 6: Individual Asset Return and Risk Contribution",
      align   = c("l","r","r","r","r")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Table 6: Individual Asset Return and Risk Contribution
Ticker Weight (%) Cum. Return (%) Ann. Volatility (%) Sharpe Ratio
QUAL 20.00 78.48 14.56 1.095
MTUM 20.00 125.75 20.11 1.230
GLD 20.00 123.84 19.89 1.228
IMTM 14.08 76.14 16.74 0.947
VWO 13.92 62.06 16.11 0.805
MSCI 4.01 33.65 27.11 0.330
ASML 2.00 156.97 41.23 0.862
EWJ 2.00 65.59 18.83 0.752
IHI 2.00 -7.33 17.00 -0.323
IQLT 2.00 49.10 14.90 0.671

5 Part V: Investment Thesis Evaluation

5.1 Does the Backtest Support the Original Hypothesis?

The core hypothesis was that a Quality-Momentum portfolio, benchmarked against ACWI, would generate: 1. Higher cumulative return than the benchmark. 2. A higher Sharpe ratio — indicating superior risk-adjusted performance. 3. Lower maximum drawdown — reflecting the quality factor’s defensive characteristics.

The backtesting results provide directional support for all three claims, though the interpretation requires care.

On cumulative return: The portfolio outperformed ACWI on a total return basis. This is consistent with the academic literature supporting quality and momentum factor premiums. However, cumulative return alone is not sufficient evidence of alpha — outperformance in a specific three-year window may reflect favorable factor conditions rather than a permanently exploitable edge.

On risk-adjusted returns: The higher Sharpe ratio suggests that the outperformance was not simply a function of taking more risk. The portfolio achieved more return per unit of volatility than the benchmark. This is the most meaningful metric for validating the strategy thesis.

On drawdown: The portfolio’s maximum drawdown being lower than the benchmark’s reflects the quality factor’s role as a defensive anchor — particularly the holdings in GLD and IHI, which provide non-correlated exposure during equity market stress events.

On alpha: The CAPM alpha is close to zero when annualized. This is an important and honest result: it means that most of the portfolio’s excess return over the risk-free rate is explained by its beta exposure to global equity markets. True abnormal return — return unexplained by market risk — is minimal. This is not unusual over a three-year window; factor premiums are typically evaluated over longer horizons (10+ years) to distinguish genuine alpha from noise.


6 Part VI: Critical Reflection

6.1 What AI Contributed That Traditional Analysis Would Have Missed

The most consequential AI contribution was the identification of hidden portfolio concentration. When reviewing the holdings table, a human analyst looking at ticker names would see QUAL and MTUM as representing “quality” and “momentum” — two distinct factors. What AI flagged was that both ETFs held Apple, Microsoft, and Nvidia as their top three positions, meaning approximately 40–50% of the underlying holdings by weight overlapped. The factor labels are different; the actual stock exposure is largely the same.

This is a form of factor crowding that is easy to miss without examining underlying holdings data, and it directly affects the portfolio’s effective diversification. The fix — introducing IQLT as an international quality ETF — addressed both this concentration and the data truncation problem caused by IBIT’s limited trading history.

AI also contributed to the macroeconomic framing: the connection between higher interest rates and quality factor performance, and the identification of Japan’s TSE corporate governance reforms as a structural catalyst for EWJ, were both developed through AI interaction and would have required more manual research to arrive at independently.

6.2 Limitations of the Backtest

6.2.1 Look-Ahead Bias

The optimization was run on the full dataset and then used to construct the backtest. This means the portfolio weights were chosen with knowledge of the outcome — a form of look-ahead bias. In a properly conducted out-of-sample backtest, the optimization would be performed only on past data, with the portfolio evaluated on subsequent returns. The current approach overstates how well the strategy would have performed if implemented in real time.

6.2.2 Survivorship Bias

The selected ETFs are all currently active and liquid. If any ETFs in the original candidate universe had been liquidated or merged over the past three years, they would be absent from the analysis. This survivorship effect slightly inflates the apparent quality of the available universe.

6.2.3 Transaction Costs and Taxes

The backtest assumes quarterly rebalancing with zero transaction costs. In reality, each rebalancing event generates trading commissions, bid-ask spread costs, and potentially capital gains tax liabilities. Momentum strategies in particular are known to incur higher turnover, which amplifies these costs. A realistic net-of-cost return would be modestly lower than the gross figures reported here.

6.2.4 Optimization Instability

Mean-variance optimization is notoriously sensitive to small changes in expected return inputs. Running the optimization again with slightly different historical windows could produce materially different weight allocations. The constrained approach used here (2%–20% bounds) dampens this instability but does not eliminate it. The optimizer’s tendency to push multiple assets to their floor (2%) suggests that the unconstrained optimal portfolio would have concentrated heavily in a small number of names.

6.2.5 Data-Snooping and Overfitting Risk

The portfolio was constructed with awareness of which factor strategies have worked historically (quality, momentum), and backtested over a period that was generally favorable for equity factors. There is a non-trivial risk that the strategy is partly overfitted to historical patterns that may not persist in the future — particularly given that factor premiums tend to compress once they become widely known and implemented.

6.2.6 Short Backtest Window

Three years is a short window for evaluating a factor strategy. Academic studies typically require decades of data to achieve statistical confidence in factor premium estimates. The results here are directionally consistent with the literature but should not be interpreted as strong empirical confirmation that the strategy generates alpha.

6.3 Summary Judgment

The Quality-Momentum strategy performed as expected in directional terms: higher return, better risk-adjusted performance, and lower drawdown relative to ACWI. However, near-zero CAPM alpha is an honest reminder that factor strategies primarily offer systematic risk premia, not true market-independent alpha. The strategy’s outperformance is largely attributable to its deliberate tilts toward quality and momentum risks, which have historically been rewarded — not to the identification of a pricing inefficiency that the market has failed to recognize.

For a real-world implementation, the key improvements would be: a rolling out-of-sample optimization protocol to eliminate look-ahead bias; explicit inclusion of transaction cost estimates; and a longer evaluation period to build statistical confidence in the factor premium claims.


7 Appendix: R Session Information

sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_Indonesia.utf8  LC_CTYPE=English_Indonesia.utf8   
## [3] LC_MONETARY=English_Indonesia.utf8 LC_NUMERIC=C                      
## [5] LC_TIME=English_Indonesia.utf8    
## 
## time zone: Asia/Taipei
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] broom_1.0.12               kableExtra_1.4.0          
##  [3] knitr_1.51                 patchwork_1.3.2           
##  [5] scales_1.4.0               lubridate_1.9.4           
##  [7] forcats_1.0.1              stringr_1.5.2             
##  [9] dplyr_1.1.4                purrr_1.1.0               
## [11] readr_2.2.0                tidyr_1.3.1               
## [13] tibble_3.3.0               ggplot2_4.0.2             
## [15] tidyverse_2.0.0            ROI.plugin.quadprog_1.0-1 
## [17] ROI_1.0-2                  PortfolioAnalytics_2.1.2  
## [19] foreach_1.5.2              PerformanceAnalytics_2.1.0
## [21] quantmod_0.4.28            TTR_0.24.4                
## [23] xts_0.14.2                 zoo_1.8-14                
## [25] tidyquant_1.0.12          
## 
## loaded via a namespace (and not attached):
##  [1] rlang_1.1.6               magrittr_2.0.3           
##  [3] ROI.plugin.glpk_1.0-0     furrr_0.3.1              
##  [5] otel_0.2.0                compiler_4.5.1           
##  [7] systemfonts_1.3.2         vctrs_0.6.5              
##  [9] lhs_1.2.1                 quadprog_1.5-8           
## [11] tune_2.0.1                pkgconfig_2.0.3          
## [13] fastmap_1.2.0             backports_1.5.0          
## [15] labeling_0.4.3            rmarkdown_2.30           
## [17] prodlim_2025.04.28        tzdb_0.5.0               
## [19] xfun_0.53                 cachem_1.1.0             
## [21] jsonlite_2.0.0            recipes_1.3.1            
## [23] parallel_4.5.1            R6_2.6.1                 
## [25] bslib_0.9.0               stringi_1.8.7            
## [27] rsample_1.3.1             RColorBrewer_1.1-3       
## [29] parallelly_1.45.1         rpart_4.1.24             
## [31] jquerylib_0.1.4           numDeriv_2016.8-1.1      
## [33] dials_1.4.2               Rcpp_1.1.0               
## [35] iterators_1.0.14          future.apply_1.20.0      
## [37] Matrix_1.7-3              splines_4.5.1            
## [39] nnet_7.3-20               timechange_0.3.0         
## [41] tidyselect_1.2.1          rstudioapi_0.17.1        
## [43] yaml_2.3.10               timeDate_4051.111        
## [45] codetools_0.2-20          curl_7.0.0               
## [47] ROI.plugin.symphony_1.0-0 listenv_0.9.1            
## [49] lattice_0.22-7            withr_3.0.2              
## [51] S7_0.2.0                  evaluate_1.0.5           
## [53] timetk_2.9.1              future_1.68.0            
## [55] survival_3.8-3            xml2_1.4.0               
## [57] pillar_1.11.0             checkmate_2.3.4          
## [59] generics_0.1.4            hms_1.1.3                
## [61] globals_0.18.0            class_7.3-23             
## [63] glue_1.8.0                slam_0.1-55              
## [65] mco_1.17                  GenSA_1.1.15             
## [67] tools_4.5.1               data.table_1.17.8        
## [69] gower_1.0.2               registry_0.5-1           
## [71] grid_4.5.1                yardstick_1.3.2          
## [73] RobStatTM_1.0.11          ipred_0.9-15             
## [75] DiceDesign_1.10           cli_3.6.5                
## [77] textshaping_1.0.3         parsnip_1.4.1            
## [79] workflows_1.3.0           pso_1.0.4                
## [81] viridisLite_0.4.2         svglite_2.2.2            
## [83] lava_1.8.1                Rsymphony_0.1-33         
## [85] gtable_0.3.6              GPfit_1.0-9              
## [87] sass_0.4.10               digest_0.6.37            
## [89] farver_2.1.2              htmltools_0.5.8.1        
## [91] Rglpk_0.6-5.1             lifecycle_1.0.5          
## [93] hardhat_1.4.2             MASS_7.3-65