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")
## ╚══════════════════════════════════════════════╝