knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE)
suppressPackageStartupMessages({
library(quantmod); library(dplyr); library(ggplot2)
library(gamlss); library(VineCopula); library(copula)
})
get_monthly_losses <- function(ticker) {
raw <- getSymbols(ticker, src = "yahoo", from = "2021-03-01", to = "2026-03-01",
auto.assign = FALSE)[, 6]
monthly <- as.numeric(Cl(to.monthly(raw)))
round(-diff(monthly) / head(monthly, -1) * 100, 4)
}
nvda_loss <- get_monthly_losses("NVDA")
aramco_loss <- get_monthly_losses("2222.SR")
byd_loss <- get_monthly_losses("BYDDF")
losses <- data.frame(NVDA = nvda_loss, Aramco = aramco_loss, BYD = byd_loss) |>
na.omit()
cat("Observations:", nrow(losses), "\n")
## Observations: 59
summary(losses)
## NVDA Aramco BYD
## Min. :-36.344 Min. :-12.3839 Min. :-36.922
## 1st Qu.:-14.755 1st Qu.: -2.8376 1st Qu.: -6.975
## Median : -5.620 Median : 0.3610 Median : -2.511
## Mean : -5.539 Mean : -0.1617 Mean : -1.554
## 3rd Qu.: 4.827 3rd Qu.: 2.9672 3rd Qu.: 6.653
## Max. : 32.027 Max. : 7.7175 Max. : 19.610
fit_best <- function(x, name) {
families <- list(NO = NO, TF = TF, GU = GU, LO = LO)
fits <- lapply(families, \(f) try(gamlss(x ~ 1, family = f, trace = FALSE), silent = TRUE))
aics <- sapply(fits, \(m) if (inherits(m, "gamlss")) AIC(m) else Inf)
best_name <- names(which.min(aics))
cat(name, "-> best family:", best_name, "| AIC:", round(min(aics), 2), "\n")
fits[[best_name]]
}
fit_nvda <- fit_best(losses$NVDA, "NVDA")
## NVDA -> best family: NO | AIC: 488.55
fit_aramco <- fit_best(losses$Aramco, "Aramco")
## Aramco -> best family: GU | AIC: 339.47
fit_byd <- fit_best(losses$BYD, "BYD")
## BYD -> best family: GU | AIC: 456.53
get_pit <- function(model, x) {
fam <- model$family[1]
pfun <- get(paste0("p", fam))
mu <- fitted(model, "mu")
sigma <- fitted(model, "sigma")
if ("nu" %in% model$parameters) {
nu <- fitted(model, "nu")
return(pfun(x, mu = mu, sigma = sigma, nu = nu))
}
pfun(x, mu = mu, sigma = sigma)
}
pit_list <- list(NVDA = get_pit(fit_nvda, losses$NVDA),
Aramco = get_pit(fit_aramco, losses$Aramco),
BYD = get_pit(fit_byd, losses$BYD))
par(mfrow = c(1, 3))
for (name in names(pit_list)) {
hist(pit_list[[name]], breaks = 10, freq = FALSE, col = "steelblue", border = "white",
main = paste("PIT –", name), xlab = "u", ylim = c(0, 2))
abline(h = 1, col = "red", lty = 2, lwd = 2)
}
par(mfrow = c(1, 1))
# pobs is the standard non-parametric route for copula estimation
U <- pobs(as.matrix(losses))
colnames(U) <- c("NVDA", "Aramco", "BYD")
par(mfrow = c(1, 3))
for (i in 1:3) {
hist(U[, i], breaks = 10, freq = FALSE, col = "steelblue", border = "white",
main = colnames(U)[i], xlab = "u", ylim = c(0, 2))
abline(h = 1, col = "red", lty = 2, lwd = 2)
}
par(mfrow = c(1, 1))
The general pair copula decomposition (PCC) for three variables without the simplifying assumption is: \[f(x_1,x_2,x_3) = f_1 \cdot f_2 \cdot f_3 \cdot c_{12}(F_1,F_2) \cdot c_{13}(F_1,F_3) \cdot c_{23|1}(F_{2|1}, F_{3|1}| x_1)\] where the conditional copula \(c_{23;1}\) and its parameters may depend on the specific value of the root \(x_1\).
To minimize this dependency and simplify the model, we select the root node as the variable with the highest sum of absolute pairwise correlations (Kendall’s \(\tau\)). This captures the most significant dependence in the first tree.
By applying the Simplifying Assumption, we assume the copula type and parameters of \(c_{23|1}\) are constant regardless of \(x_1\), yielding the operational C-vine formula: \[f(x_1,x_2,x_3) = f_1 \cdot f_2 \cdot f_3 \cdot c_{12}(F_1,F_2) \cdot c_{13}(F_1,F_3) \cdot c_{23|1}(F_{2|1}, F_{3|1})\]
tau_matrix <- TauMatrix(U)
tau_sums <- colSums(abs(tau_matrix) - diag(3))
cat("Sum of |tau| per variable:\n")
## Sum of |tau| per variable:
print(round(tau_sums, 4))
## NVDA Aramco BYD
## 0.1788 0.0725 0.2221
root <- names(which.max(tau_sums))
cat("Suggested root:", root, "\n")
## Suggested root: BYD
# Tree 1: unconditional pairs (BYD as root)
bc_byd_nvda <- BiCopSelect(U[,"BYD"], U[,"NVDA"], familyset = NA, selectioncrit = "AIC",
indeptest = TRUE, level = 0.05)
bc_byd_aramco <- BiCopSelect(U[,"BYD"], U[,"Aramco"], familyset = NA, selectioncrit = "AIC",
indeptest = TRUE, level = 0.05)
# H-functions: F(NVDA|BYD), F(Aramco|BYD)
h_nvda_byd <- pmax(pmin(BiCopHfunc2(U[,"BYD"], U[,"NVDA"], bc_byd_nvda), 1-1e-10), 1e-10)
h_aramco_byd <- pmax(pmin(BiCopHfunc2(U[,"BYD"], U[,"Aramco"], bc_byd_aramco), 1-1e-10), 1e-10)
# Tree 2: conditional pair (NVDA – Aramco | BYD)
bc_nvda_aramco_given_byd <- BiCopSelect(h_nvda_byd, h_aramco_byd, familyset = NA,
selectioncrit = "AIC", indeptest = TRUE, level = 0.05)
cat("=== C-VINE STRUCTURE (root: BYD) ===\n")
## === C-VINE STRUCTURE (root: BYD) ===
cat(sprintf("c_BYD-NVDA %-20s tau=%+.4f\n", bc_byd_nvda$familyname, bc_byd_nvda$tau))
## c_BYD-NVDA Independence tau=+0.0000
cat(sprintf("c_BYD-Aramco %-20s tau=%+.4f\n", bc_byd_aramco$familyname, bc_byd_aramco$tau))
## c_BYD-Aramco Independence tau=+0.0000
cat(sprintf("c_NVDA-Aramco|BYD %-20s tau=%+.4f\n", bc_nvda_aramco_given_byd$familyname, bc_nvda_aramco_given_byd$tau))
## c_NVDA-Aramco|BYD t tau=+0.9910
par(mfrow = c(1, 3))
plot(U[,"BYD"], U[,"NVDA"], pch = 19, col = rgb(.2,.4,.8,.6), cex = .8,
xlab = "BYD", ylab = "NVDA",
main = sprintf("c_BYD-NVDA: %s", bc_byd_nvda$familyname)); grid()
plot(U[,"BYD"], U[,"Aramco"], pch = 19, col = rgb(.8,.3,.2,.6), cex = .8,
xlab = "BYD", ylab = "Aramco",
main = sprintf("c_BYD-Aramco: %s", bc_byd_aramco$familyname)); grid()
plot(h_nvda_byd, h_aramco_byd, pch = 19, col = rgb(.2,.7,.3,.6), cex = .8,
xlab = "F(NVDA|BYD)", ylab = "F(Aramco|BYD)",
main = sprintf("c_NVDA-Aramco|BYD: %s", bc_nvda_aramco_given_byd$familyname)); grid()
par(mfrow = c(1, 1))
set.seed(42)
N <- 100000
# Step 1: simulate (u_BYD, u_NVDA) from Tree 1 pair c_BYD-NVDA
sim_byd_nvda <- BiCopSim(N, bc_byd_nvda)
u_byd <- sim_byd_nvda[, 1]
u_nvda <- sim_byd_nvda[, 2]
# Step 2: simulate u_Aramco using Tree 1 pair c_BYD-Aramco and Tree 2 pair c_NVDA-Aramco|BYD
# Draw v from Tree 2 conditional copula, then invert h-function from Tree 1
v_tree2 <- BiCopHinv2(
BiCopHfunc2(u_byd, u_nvda, bc_byd_nvda), # F(NVDA|BYD)
runif(N),
bc_nvda_aramco_given_byd) # invert c_NVDA-Aramco|BYD
u_aramco <- BiCopHinv2(u_byd, v_tree2, bc_byd_aramco) # invert c_BYD-Aramco
sim_U <- cbind(u_nvda, u_aramco, u_byd)
colnames(sim_U) <- c("NVDA", "Aramco", "BYD")
# Back-transform to loss scale using empirical quantiles
q_nvda <- quantile(losses$NVDA, probs = u_nvda, type = 1)
q_aramco <- quantile(losses$Aramco, probs = u_aramco, type = 1)
q_byd <- quantile(losses$BYD, probs = u_byd, type = 1)
portfolio_loss <- (q_nvda + q_aramco + q_byd) / 3
VaR_95 <- quantile(portfolio_loss, 0.95)
ES_95 <- mean(portfolio_loss[portfolio_loss > VaR_95])
cat(sprintf("VaR (95%%): %.4f%%\nES (95%%): %.4f%%\n", VaR_95, ES_95))
## VaR (95%): 9.0054%
## ES (95%): 11.4795%
hist(portfolio_loss, breaks = 100, freq = FALSE, col = "lightblue", border = "white",
main = "Simulated Portfolio Loss Distribution",
xlab = "Monthly Loss (%)")
abline(v = VaR_95, col = "red", lwd = 2, lty = 2)
abline(v = ES_95, col = "darkred", lwd = 2, lty = 1)
legend("topright", legend = c(sprintf("VaR 95%% = %.2f%%", VaR_95),
sprintf("ES 95%% = %.2f%%", ES_95)),
col = c("red", "darkred"), lty = c(2, 1), lwd = 2, bty = "n")