library(tidyquant)
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
## ── Attaching core tidyquant packages ─────────────────────── tidyquant 1.0.11 ──
## ✔ PerformanceAnalytics 2.0.8      ✔ TTR                  0.24.4
## ✔ quantmod             0.4.28     ✔ xts                  0.14.1
## ── Conflicts ────────────────────────────────────────── tidyquant_conflicts() ──
## ✖ zoo::as.Date()                 masks base::as.Date()
## ✖ zoo::as.Date.numeric()         masks base::as.Date.numeric()
## ✖ PerformanceAnalytics::legend() masks graphics::legend()
## ✖ quantmod::summary()            masks base::summary()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(PortfolioAnalytics)
## Warning: package 'PortfolioAnalytics' was built under R version 4.5.2
## Loading required package: foreach
## Registered S3 method overwritten by 'PortfolioAnalytics':
##   method           from
##   print.constraint ROI
library(quadprog)
library(ROI); library(ROI.plugin.quadprog); library(ROI.plugin.glpk)
## ROI: R Optimization Infrastructure
## Registered solver plugins: nlminb, symphony, glpk, quadprog.
## Default solver: auto.
## 
## Attaching package: 'ROI'
## 
## The following objects are masked from 'package:PortfolioAnalytics':
## 
##     is.constraint, objective
library(PerformanceAnalytics)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.0     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ purrr::accumulate() masks foreach::accumulate()
## ✖ dplyr::filter()     masks stats::filter()
## ✖ dplyr::first()      masks xts::first()
## ✖ dplyr::lag()        masks stats::lag()
## ✖ dplyr::last()       masks xts::last()
## ✖ purrr::when()       masks foreach::when()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
library(ggplot2)
library(ggrepel)
## Warning: package 'ggrepel' was built under R version 4.5.2
library(patchwork)
library(gridExtra)
## 
## Attaching package: 'gridExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     combine
library(corrplot)
## corrplot 0.95 loaded
library(RColorBrewer)
COL_PORT  <- "#1565C0"
COL_BENCH <- "#E53935"
COL_ACC   <- "#00897B"
GRID      <- "#EEEEEE"
 
theme_pro <- function() {
  theme_minimal(base_size = 12) +
  theme(
    plot.title       = element_text(face = "bold", size = 14, colour = "#1A1A2E"),
    plot.subtitle    = element_text(size = 10, colour = "#555555"),
    plot.caption     = element_text(size = 8,  colour = "#999999", hjust = 0),
    axis.title       = element_text(size = 10, colour = "#333333"),
    axis.text        = element_text(size = 9,  colour = "#555555"),
    panel.grid.major = element_line(colour = GRID, linewidth = 0.4),
    panel.grid.minor = element_blank(),
    legend.position  = "bottom",
    legend.title     = element_blank(),
    plot.background  = element_rect(fill = "white", colour = NA),
    panel.background = element_rect(fill = "white", colour = NA)
  )
}
tickers <- c("AAPL","MSFT","GOOGL","AMZN","TSLA",
             "META","NVDA","JPM","V","UNH")
benchmark_ticker <- "SPY"
start_date <- Sys.Date() - years(3)
 
cat(">>> Downloading data...\n")
## >>> Downloading data...
prices_raw <- tq_get(c(tickers, benchmark_ticker),
                     from = start_date, get = "stock.prices")
 
prices_port  <- prices_raw %>% filter(symbol != benchmark_ticker)
prices_bench <- prices_raw %>% filter(symbol == benchmark_ticker)
returns_long <- prices_port %>%
  group_by(symbol) %>%
  tq_transmute(select = adjusted, mutate_fun = periodReturn,
               period = "daily", col_rename = "Ra")
 
returns_wide <- returns_long %>%
  pivot_wider(names_from = symbol, values_from = Ra) %>% drop_na()
 
returns_xts <- xts(returns_wide[,-1], order.by = as.Date(returns_wide$date))
 
bench_ret <- prices_bench %>%
  tq_transmute(select = adjusted, mutate_fun = periodReturn,
               period = "daily", col_rename = "Benchmark") %>%
  { xts(.[,"Benchmark",drop=FALSE], order.by = as.Date(.[["date"]])) }
 
common      <- intersect(index(returns_xts), index(bench_ret))
returns_xts <- returns_xts[common,]
bench_ret   <- bench_ret[common,]
 
cat(">>> Data ready:", nrow(returns_xts), "trading days\n")
## >>> Data ready: 750 trading days
ps <- portfolio.spec(assets = tickers)
ps <- add.constraint(ps, type = "full_investment")
ps <- add.constraint(ps, type = "long_only")
ps <- add.constraint(ps, type = "box", min = 0.02, max = 0.20)
ps <- add.objective(ps, type = "return", name = "mean")
ps <- add.objective(ps, type = "risk",   name = "StdDev")
 
cat(">>> Optimising portfolio...\n")
## >>> Optimising portfolio...
opt <- optimize.portfolio(R = returns_xts, portfolio = ps,
                          optimize_method = "ROI", maxSR = TRUE,
                          solver = "quadprog", trace = FALSE)
w <- extractWeights(opt)
port_ret <- Return.portfolio(returns_xts, weights = w, rebalance_on = "years")
colnames(port_ret) <- "AI_Portfolio"
combined <- merge.xts(port_ret, bench_ret)
 
Rf <- 0.05 / 252
ann   <- table.AnnualizedReturns(combined, Rf = Rf)
mdd   <- maxDrawdown(combined)
sr    <- SharpeRatio.annualized(combined, Rf = Rf)
alpha <- CAPM.alpha(port_ret, bench_ret, Rf = Rf) * 252
beta  <- CAPM.beta(port_ret,  bench_ret, Rf = Rf)
calr  <- table.CalendarReturns(combined)
 
cat("\n========== PERFORMANCE SUMMARY ==========\n")
## 
## ========== PERFORMANCE SUMMARY ==========
print(ann); print(sr); print(mdd)
##                           AI_Portfolio Benchmark
## Annualized Return               0.4011    0.2285
## Annualized Std Dev              0.2113    0.1513
## Annualized Sharpe (Rf=5%)       1.5750    1.1141
##                                 AI_Portfolio Benchmark
## Annualized Sharpe Ratio (Rf=5%)     1.575014  1.114073
##                AI_Portfolio Benchmark
## Worst Drawdown    0.2194165 0.1875524
cat(sprintf("Alpha (ann.): %.2f%%  |  Beta: %.4f\n", alpha * 100, beta))
## Alpha (ann.): 10.35%  |  Beta: 1.2329
cat("\nCalendar Returns:\n"); print(calr)
## 
## Calendar Returns:
##       Jan  Feb Mar  Apr  May  Jun  Jul  Aug  Sep  Oct  Nov  Dec AI_Portfolio
## 2023   NA   NA  NA   NA -1.5  1.8  0.3 -0.2 -0.5  0.2 -0.5 -0.2         -0.7
## 2024 -2.7  0.8 0.0 -1.5  0.2 -0.6  4.3  1.1  0.4 -2.7  0.9 -1.1         -1.1
## 2025 -0.5  2.2 0.7  0.1 -0.5  0.5 -0.9 -0.6  0.5 -0.1  0.2 -0.5          1.1
## 2026 -0.9 -1.1 3.8  0.9 -0.4   NA   NA   NA   NA   NA   NA   NA          2.3
##      Benchmark
## 2023       1.2
## 2024      -1.1
## 2025       1.6
## 2026       3.5
cum_df <- data.frame(
  date      = index(combined),
  Portfolio = as.numeric(cumprod(1 + combined[,"AI_Portfolio"])) * 10000,
  Benchmark = as.numeric(cumprod(1 + combined[,"Benchmark"]))    * 10000
) %>% pivot_longer(-date, names_to = "Strategy", values_to = "Value")
 
p1 <- ggplot(cum_df, aes(x = date, y = Value, colour = Strategy)) +
  geom_line(linewidth = 1.1) +
  geom_hline(yintercept = 10000, linetype = "dashed",
             colour = "#AAAAAA", linewidth = 0.5) +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(title    = "Chart 1: Cumulative Wealth Growth",
       subtitle = "Growth of $10,000 invested — AI Portfolio vs S&P 500",
       x = NULL, y = "Portfolio Value (USD)",
       caption  = "Source: Yahoo Finance via tidyquant") +
  theme_pro()
print(p1)

dd_df <- data.frame(
  date      = index(combined),
  Portfolio = as.numeric(Drawdowns(port_ret)),
  Benchmark = as.numeric(Drawdowns(bench_ret))
) %>% pivot_longer(-date, names_to = "Strategy", values_to = "Drawdown")
 
p2 <- ggplot(dd_df, aes(x = date, y = Drawdown,
                         fill = Strategy, colour = Strategy)) +
  geom_area(alpha = 0.25, position = "identity") +
  geom_line(linewidth = 0.8) +
  scale_fill_manual(values   = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_y_continuous(labels = percent_format()) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(title    = "Chart 2: Drawdown Analysis",
       subtitle = "Depth and duration of portfolio losses",
       x = NULL, y = "Drawdown (%)",
       caption  = "Shaded areas show underwater periods") +
  theme_pro()
print(p2)

w_df <- tibble(Ticker = names(w), Weight = round(w * 100, 2)) %>%
        arrange(desc(Weight)) %>%
        mutate(Ticker = factor(Ticker, levels = Ticker))
 
p3 <- ggplot(w_df, aes(x = Ticker, y = Weight, fill = Weight)) +
  geom_col(width = 0.65, show.legend = FALSE) +
  geom_text(aes(label = paste0(Weight, "%")), vjust = -0.5,
            size = 3.2, colour = "#333333", fontface = "bold") +
  scale_fill_gradient(low = "#90CAF9", high = COL_PORT) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.18)),
                     labels = function(x) paste0(x, "%")) +
  labs(title    = "Chart 3: Optimised Portfolio Weights",
       subtitle = "Maximum Sharpe Ratio | Constraints: 2%–20% per asset",
       x = NULL, y = "Weight (%)",
       caption  = "Solver: ROI quadprog") +
  theme_pro() +
  theme(axis.text.x = element_text(face = "bold", size = 10))
print(p3)

roll_sr <- rollapply(combined, width = 252,
  FUN = function(x) SharpeRatio.annualized(x, Rf = Rf)[1],
  by.column = TRUE, align = "right")
colnames(roll_sr) <- c("Portfolio","Benchmark")
 
sr_df <- data.frame(
  date      = index(roll_sr),
  Portfolio = as.numeric(roll_sr[,"Portfolio"]),
  Benchmark = as.numeric(roll_sr[,"Benchmark"])
) %>% drop_na() %>%
  pivot_longer(-date, names_to = "Strategy", values_to = "Sharpe")
 
p4 <- ggplot(sr_df, aes(x = date, y = Sharpe, colour = Strategy)) +
  geom_line(linewidth = 1.0) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "#AAAAAA") +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(title    = "Chart 4: Rolling 12-Month Sharpe Ratio",
       subtitle = "Risk-adjusted performance over time",
       x = NULL, y = "Sharpe Ratio (annualised)",
       caption  = "Risk-free rate: 5% p.a.") +
  theme_pro()
print(p4)

monthly_port <- to.monthly(port_ret, indexAt = "lastof", OHLC = FALSE)
monthly_df <- data.frame(
  date   = index(monthly_port),
  Return = as.numeric(monthly_port)
) %>% mutate(Year  = year(date),
             Month = month(date, label = TRUE, abbr = TRUE))
 
p5 <- ggplot(monthly_df, aes(x = Month, y = factor(Year), fill = Return)) +
  geom_tile(colour = "white", linewidth = 0.6) +
  geom_text(aes(label = percent(Return, accuracy = 0.1)),
            size = 3, colour = "white", fontface = "bold") +
  scale_fill_gradient2(low = COL_BENCH, mid = "#F5F5F5",
                       high = COL_PORT, midpoint = 0,
                       labels = percent_format()) +
  labs(title    = "Chart 5: Monthly Returns Heatmap",
       subtitle = "AI Portfolio — month-by-month performance",
       x = NULL, y = NULL, fill = "Return",
       caption  = "Blue = positive | Red = negative") +
  theme_pro() +
  theme(legend.position = "right",
        axis.text.y     = element_text(face = "bold", size = 11))
print(p5)

dist_df <- data.frame(
  Portfolio = as.numeric(port_ret),
  Benchmark = as.numeric(bench_ret)
) %>% pivot_longer(everything(),
                   names_to  = "Strategy",
                   values_to = "Return") %>% drop_na()
 
p6 <- ggplot(dist_df, aes(x = Return, fill = Strategy, colour = Strategy)) +
  geom_histogram(aes(y = after_stat(density)), bins = 80,
                 alpha = 0.4, position = "identity") +
  geom_density(linewidth = 1.0, alpha = 0) +
  scale_fill_manual(values   = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_x_continuous(labels = percent_format()) +
  labs(title    = "Chart 6: Daily Return Distribution",
       subtitle = "Histogram + density of daily returns",
       x = "Daily Return", y = "Density",
       caption  = "Fatter right tail = positive skew") +
  theme_pro()
print(p6)

cor_matrix <- cor(returns_xts)
corrplot(cor_matrix,
         method      = "color",
         type        = "upper",
         tl.col      = "#333333",
         tl.srt      = 45,
         tl.cex      = 0.85,
         addCoef.col = "white",
         number.cex  = 0.65,
         col         = colorRampPalette(c(COL_BENCH, "white", COL_PORT))(200),
         title       = "Chart 7: Asset Correlation Matrix",
         mar         = c(0, 0, 2, 0))

stock_cum <- prices_port %>%
  group_by(symbol) %>%
  tq_transmute(select = adjusted, mutate_fun = periodReturn,
               period = "daily", col_rename = "ret") %>%
  mutate(cum_ret = cumprod(1 + ret) - 1)
 
end_labels <- stock_cum %>% group_by(symbol) %>% slice_tail(n = 1)
 
p8 <- ggplot(stock_cum, aes(x = date, y = cum_ret, colour = symbol)) +
  geom_line(linewidth = 0.8, alpha = 0.85) +
  geom_label_repel(data       = end_labels,
                   aes(label  = paste0(symbol, " ",
                                percent(cum_ret, accuracy = 1))),
                   size       = 2.8, nudge_x = 10,
                   show.legend = FALSE, max.overlaps = 20,
                   segment.size = 0.3) +
  scale_y_continuous(labels = percent_format()) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  scale_colour_manual(values =
    colorRampPalette(brewer.pal(10, "Paired"))(10)) +
  labs(title    = "Chart 8: Individual Stock Cumulative Returns",
       subtitle = "3-year total return per holding",
       x = NULL, y = "Cumulative Return",
       caption  = "Labels show final cumulative return") +
  theme_pro() +
  theme(legend.position = "none")
print(p8)

rr_df <- tibble(
  Symbol = tickers,
  Return = as.numeric(Return.annualized(returns_xts, scale = 252)),
  Risk   = as.numeric(StdDev.annualized(returns_xts, scale = 252))
)
port_rr  <- tibble(Symbol = "Portfolio",
  Return = as.numeric(Return.annualized(port_ret,  scale = 252)),
  Risk   = as.numeric(StdDev.annualized(port_ret,  scale = 252)))
bench_rr <- tibble(Symbol = "S&P 500",
  Return = as.numeric(Return.annualized(bench_ret, scale = 252)),
  Risk   = as.numeric(StdDev.annualized(bench_ret, scale = 252)))
 
rr_all <- bind_rows(rr_df, port_rr, bench_rr) %>%
  mutate(Type = case_when(
    Symbol == "Portfolio" ~ "Portfolio",
    Symbol == "S&P 500"  ~ "Benchmark",
    TRUE                  ~ "Stock"))
 
p9 <- ggplot(rr_all, aes(x = Risk, y = Return,
                          colour = Type, size = Type)) +
  geom_point(alpha = 0.85) +
  geom_label_repel(aes(label = Symbol), size = 3,
                   show.legend = FALSE, max.overlaps = 20,
                   segment.size = 0.3) +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH,
                                 "Stock"     = COL_ACC)) +
  scale_size_manual(values   = c("Portfolio" = 5,
                                 "Benchmark" = 5,
                                 "Stock"     = 3)) +
  scale_x_continuous(labels = percent_format()) +
  scale_y_continuous(labels = percent_format()) +
  labs(title    = "Chart 9: Risk-Return Scatter",
       subtitle = "Annualised return vs annualised volatility per asset",
       x = "Annualised Risk (Std Dev)", y = "Annualised Return",
       caption  = "Top-left = high return, low risk (ideal)") +
  theme_pro()
print(p9)

roll_ret <- rollapply(combined, width = 252,
  FUN = function(x) Return.annualized(x, scale = 252)[1],
  by.column = TRUE, align = "right")
colnames(roll_ret) <- c("Portfolio","Benchmark")
 
roll_ret_df <- data.frame(
  date      = index(roll_ret),
  Portfolio = as.numeric(roll_ret[,"Portfolio"]),
  Benchmark = as.numeric(roll_ret[,"Benchmark"])
) %>% drop_na() %>%
  pivot_longer(-date, names_to = "Strategy", values_to = "Return")
 
p10 <- ggplot(roll_ret_df, aes(x = date, y = Return, colour = Strategy)) +
  geom_line(linewidth = 1.0) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "#AAAAAA") +
  scale_colour_manual(values = c("Portfolio" = COL_PORT,
                                 "Benchmark" = COL_BENCH)) +
  scale_y_continuous(labels = percent_format()) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(title    = "Chart 10: Rolling 12-Month Annualised Returns",
       subtitle = "Consistency of outperformance over time",
       x = NULL, y = "Annualised Return",
       caption  = "Rolling 252-trading-day window") +
  theme_pro()
print(p10)

# ══════════════════════════════════════════════════════════════
# TABLE 1 — Full Performance Summary
# ══════════════════════════════════════════════════════════════
cat("\n")
cat("╔══════════════════════════════════════════════════════════╗\n")
## ╔══════════════════════════════════════════════════════════╗
cat("║          TABLE 1: FULL PERFORMANCE SUMMARY              ║\n")
## ║          TABLE 1: FULL PERFORMANCE SUMMARY              ║
cat("╠══════════════════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════════════════╣
perf_table <- data.frame(
  Metric    = c("Annualised Return","Annualised Std Dev",
                "Sharpe Ratio","Maximum Drawdown",
                "Alpha (ann.)","Beta","Cumulative Return"),
  Portfolio = c(
    percent(as.numeric(ann["Annualized Return", "AI_Portfolio"]),  0.1),
    percent(as.numeric(ann["Annualized Std Dev","AI_Portfolio"]),  0.1),
    round(as.numeric(sr["Annualized Sharpe Ratio (Rf=5%)","AI_Portfolio"]), 2),
    percent(as.numeric(mdd["AI_Portfolio"]) * -1, 0.1),
    percent(alpha, 0.1),
    round(beta, 2),
    percent(as.numeric(last(cumprod(1 + port_ret))) - 1, 0.1)
  ),
  Benchmark = c(
    percent(as.numeric(ann["Annualized Return", "Benchmark"]),  0.1),
    percent(as.numeric(ann["Annualized Std Dev","Benchmark"]),  0.1),
    round(as.numeric(sr["Annualized Sharpe Ratio (Rf=5%)","Benchmark"]), 2),
    percent(as.numeric(mdd["Benchmark"]) * -1, 0.1),
    "—", "1.00",
    percent(as.numeric(last(cumprod(1 + bench_ret))) - 1, 0.1)
  )
)
print(perf_table, row.names = FALSE)
##              Metric Portfolio Benchmark
##   Annualised Return     40.1%     22.8%
##  Annualised Std Dev     21.1%     15.1%
##        Sharpe Ratio      1.58      1.11
##    Maximum Drawdown      <NA>      <NA>
##        Alpha (ann.)     10.4%         —
##                Beta      1.23      1.00
##   Cumulative Return    172.8%     84.5%
cat("╚══════════════════════════════════════════════════════════╝\n")
## ╚══════════════════════════════════════════════════════════╝
# ══════════════════════════════════════════════════════════════
# TABLE 2 — Portfolio Holdings Detail
# ══════════════════════════════════════════════════════════════
cat("\n")
cat("╔══════════════════════════════════════════════════════════════════╗\n")
## ╔══════════════════════════════════════════════════════════════════╗
cat("║              TABLE 2: PORTFOLIO HOLDINGS DETAIL                 ║\n")
## ║              TABLE 2: PORTFOLIO HOLDINGS DETAIL                 ║
cat("╠══════════════════════════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════════════════════════╣
holdings_table <- tibble(
  Ticker      = names(w),
  Weight      = paste0(round(w * 100, 1), "%"),
  AnnReturn   = percent(as.numeric(
    Return.annualized(returns_xts, scale = 252)), 0.1),
  AnnVol      = percent(as.numeric(
    StdDev.annualized(returns_xts, scale = 252)), 0.1),
  SharpeRatio = round(as.numeric(
    SharpeRatio.annualized(returns_xts, Rf = Rf)), 2)
) %>% rename("Ann. Return" = AnnReturn,
             "Ann. Vol"    = AnnVol,
             "Sharpe"      = SharpeRatio) %>%
  arrange(desc(parse_number(Weight)))
 
print(as.data.frame(holdings_table), row.names = FALSE)
##  Ticker Weight Ann. Return Ann. Vol Sharpe
##   GOOGL    20%       46.2%    29.6%   1.32
##    NVDA    20%       77.7%    46.9%   1.47
##     JPM    20%       34.1%    22.7%   1.22
##       V    20%       14.5%    19.7%   0.45
##    AAPL   8.2%       21.5%    25.8%   0.60
##    META     3%       33.2%    36.0%   0.74
##     UNH   2.7%       -5.1%    36.8%  -0.26
##    MSFT     2%        8.9%    23.5%   0.15
##    AMZN     2%       30.7%    30.9%   0.79
##    TSLA     2%       30.4%    57.9%   0.42
cat("╚══════════════════════════════════════════════════════════════════╝\n")
## ╚══════════════════════════════════════════════════════════════════╝
# ══════════════════════════════════════════════════════════════
# TABLE 3 — Calendar Year Returns
# ══════════════════════════════════════════════════════════════
cat("\n")
cat("╔══════════════════════════════════════════════╗\n")
## ╔══════════════════════════════════════════════╗
cat("║       TABLE 3: CALENDAR YEAR RETURNS        ║\n")
## ║       TABLE 3: CALENDAR YEAR RETURNS        ║
cat("╠══════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════╣
print(calr)
##       Jan  Feb Mar  Apr  May  Jun  Jul  Aug  Sep  Oct  Nov  Dec AI_Portfolio
## 2023   NA   NA  NA   NA -1.5  1.8  0.3 -0.2 -0.5  0.2 -0.5 -0.2         -0.7
## 2024 -2.7  0.8 0.0 -1.5  0.2 -0.6  4.3  1.1  0.4 -2.7  0.9 -1.1         -1.1
## 2025 -0.5  2.2 0.7  0.1 -0.5  0.5 -0.9 -0.6  0.5 -0.1  0.2 -0.5          1.1
## 2026 -0.9 -1.1 3.8  0.9 -0.4   NA   NA   NA   NA   NA   NA   NA          2.3
##      Benchmark
## 2023       1.2
## 2024      -1.1
## 2025       1.6
## 2026       3.5
cat("╚══════════════════════════════════════════════╝\n")
## ╚══════════════════════════════════════════════╝