1. Install Required Packages

Run this chunk once if packages are not yet installed.

# All packages below are available on CRAN
install.packages(c(
  "tidyquant",
  "PerformanceAnalytics",
  "xts",
  "tidyverse",
  "lubridate",
  "timetk"
))

# NOTE: PortfolioAnalytics is NOT on CRAN. It is not used in this backtest.
# If you need it in the future, install from GitHub:
# remotes::install_github("braverock/PortfolioAnalytics")

2. Load Libraries

library(tidyquant)
library(PerformanceAnalytics)
library(xts)
library(tidyverse)
library(lubridate)
library(timetk)   # provides tk_xts()

3. Define Portfolio

tickers <- c(
  "NVDA",
  "MSFT",
  "META",
  "AMZN",
  "AAPL",
  "V",
  "JNJ",
  "XOM"
)

# Portfolio weights (must sum to 1.0)
weights <- c(
  0.22,  # NVDA
  0.18,  # MSFT
  0.14,  # META
  0.12,  # AMZN
  0.10,  # AAPL
  0.06,  # V
  0.05,  # JNJ
  0.13   # XOM
)

# Sanity check
stopifnot("Weights must sum to 1" = abs(sum(weights) - 1) < 1e-8)

# Benchmark
benchmark <- "SPY"

4. Download Historical Data

start_date <- Sys.Date() - years(3)

prices <- tq_get(
  tickers,
  from = start_date,
  get  = "stock.prices"
)

benchmark_prices <- tq_get(
  benchmark,
  from = start_date,
  get  = "stock.prices"
)

5. Convert Prices to Returns

# Portfolio stock returns (daily log returns)
returns_tbl <- prices %>%
  group_by(symbol) %>%
  tq_transmute(
    select     = adjusted,
    mutate_fun = periodReturn,
    period     = "daily",
    type       = "log",
    col_rename = "returns"
  )

# Convert to wide format
returns_wide <- returns_tbl %>%
  select(date, symbol, returns) %>%
  pivot_wider(
    names_from  = symbol,
    values_from = returns
  )

# BUG FIX: tq_as_xts() does not exist; use tk_xts() from timetk instead
returns_xts <- tk_xts(returns_wide, date_var = date)

# Remove missing values
returns_xts <- na.omit(returns_xts)

# BUG FIX: pivot_wider does not guarantee column order matches tickers/weights.
# Reorder columns explicitly so weights align correctly with Return.portfolio().
returns_xts <- returns_xts[, tickers]

6. Benchmark Returns

benchmark_returns_tbl <- benchmark_prices %>%
  tq_transmute(
    select     = adjusted,
    mutate_fun = periodReturn,
    period     = "daily",
    type       = "log",
    col_rename = "SPY"
  )

# BUG FIX: tq_as_xts() does not exist; use tk_xts() from timetk instead
benchmark_returns <- tk_xts(benchmark_returns_tbl, date_var = date)

benchmark_returns <- na.omit(benchmark_returns)

7. Portfolio Returns

portfolio_returns <- Return.portfolio(
  R            = returns_xts,
  weights      = weights,
  rebalance_on = "months"
)

colnames(portfolio_returns) <- "AI_Portfolio"

8. Merge Portfolio & Benchmark

combined_returns <- merge(
  portfolio_returns,
  benchmark_returns
)

combined_returns <- na.omit(combined_returns)

9. Performance Summary Chart

charts.PerformanceSummary(
  combined_returns,
  main = "AI Portfolio vs SPY Performance"
)

10. Cumulative Returns

cum_returns <- Return.cumulative(combined_returns)
print(cum_returns)
##                   AI_Portfolio       SPY
## Cumulative Return     1.095816 0.8063895

11. Annualized Returns

annualized_returns <- table.AnnualizedReturns(combined_returns)
print(annualized_returns)
##                           AI_Portfolio    SPY
## Annualized Return               0.2818 0.2195
## Annualized Std Dev              0.2067 0.1507
## Annualized Sharpe (Rf=0%)       1.3638 1.4563

12. Sharpe Ratio

sharpe_ratio <- SharpeRatio.annualized(
  combined_returns,
  Rf    = 0,
  scale = 252
)
print(sharpe_ratio)
##                                 AI_Portfolio      SPY
## Annualized Sharpe Ratio (Rf=0%)     1.363758 1.456283

13. Maximum Drawdown

mdd <- maxDrawdown(combined_returns)
print(mdd)
##                AI_Portfolio       SPY
## Worst Drawdown    0.2266467 0.1920899

14. Alpha & Beta

alpha <- CAPM.alpha(
  Ra = portfolio_returns,
  Rb = benchmark_returns,
  Rf = 0
)

beta <- CAPM.beta(
  Ra = portfolio_returns,
  Rb = benchmark_returns,
  Rf = 0
)

cat("Alpha:\n"); print(alpha)
## Alpha:
## [1] 7.532374e-05
cat("\nBeta:\n");  print(beta)
## 
## Beta:
## [1] 1.194913

15. Additional Risk Metrics

# BUG FIX: SortinoRatio() and CalmarRatio() return matrix objects with different
# dimensions than StdDev.annualized(), making rbind() fail or silently misalign.
# Compute each separately and combine into a clean data frame instead.

risk_metrics <- data.frame(
  Metric = c("Annualized Std Dev", "Sortino Ratio", "Calmar Ratio"),

  AI_Portfolio = c(
    as.numeric(StdDev.annualized(portfolio_returns)),
    as.numeric(SortinoRatio(portfolio_returns)),
    as.numeric(CalmarRatio(portfolio_returns))
  ),

  SPY = c(
    as.numeric(StdDev.annualized(benchmark_returns)),
    as.numeric(SortinoRatio(benchmark_returns)),
    as.numeric(CalmarRatio(benchmark_returns))
  )
)

print(risk_metrics)
##               Metric AI_Portfolio       SPY
## 1 Annualized Std Dev    0.2066578 0.1507106
## 2      Sortino Ratio    0.1212481 0.1295169
## 3       Calmar Ratio    1.2434829 1.1425762

16. Rolling Drawdown Chart

chart.Drawdown(
  combined_returns,
  main = "Portfolio Drawdown Analysis"
)

17. Rolling Sharpe Ratio

chart.RollingPerformance(
  portfolio_returns,
  width = 126,
  FUN   = "SharpeRatio.annualized",
  main  = "Rolling Sharpe Ratio (6-Month Window)"
)

18. Save Performance Tables

# BUG FIX: maxDrawdown() returns a named numeric vector, not a matrix.
# Wrap it in as.data.frame() via t() so it saves cleanly as a CSV row.
write.csv(as.data.frame(cum_returns),       "cumulative_returns.csv")
write.csv(as.data.frame(annualized_returns), "annualized_returns.csv")
write.csv(as.data.frame(sharpe_ratio),       "sharpe_ratio.csv")
write.csv(as.data.frame(t(mdd)),             "max_drawdown.csv")  # BUG FIX: t() to make it a proper row

cat("All CSV outputs saved.\n")
## All CSV outputs saved.

19. Summary

cat("====================================\n")
## ====================================
cat("BACKTEST COMPLETED SUCCESSFULLY\n")
## BACKTEST COMPLETED SUCCESSFULLY
cat("====================================\n\n")
## ====================================
cat("Generated Outputs:\n")
## Generated Outputs:
cat("- Performance Comparison Chart\n")
## - Performance Comparison Chart
cat("- Cumulative Returns\n")
## - Cumulative Returns
cat("- Annualized Returns\n")
## - Annualized Returns
cat("- Sharpe Ratio\n")
## - Sharpe Ratio
cat("- Maximum Drawdown\n")
## - Maximum Drawdown
cat("- Alpha & Beta\n")
## - Alpha & Beta
cat("- Additional Risk Metrics (Std Dev, Sortino, Calmar)\n")
## - Additional Risk Metrics (Std Dev, Sortino, Calmar)
cat("- Rolling Sharpe Ratio Chart\n")
## - Rolling Sharpe Ratio Chart
cat("- Drawdown Analysis Chart\n")
## - Drawdown Analysis Chart