Con este proceso vamos a llevar a cabo un análisis de cobertura europea para las acciones NU, ONON y NVDA, utilizando datos recopilados desde el 01/06/2022 al 06/04/2025.
Al final, vamos a considerar una opción de estás 3 acciones, que liquide de forma trimestral hasta la fecha de inversión máxima de la opción (o máximo dos años en el futuro), donde el precio strike se determinará conforme los criterios de liquidez (bid-ask), volatilidad implícita y open interest. Se realizará un “rolling” al finalizar cada contrato, ajustando el precio de prima cada trimestre. La tasa libre de riesgo se establecerá a un trimestre, basada en los bonos del Tesoro (Tbond).
Antes de iniciar con el proceso de analisis, primero vamos a instalar y cargar los paquetes necesarios
install.packages(c(“tidyquant”,“plotly”,“timetk”,“tibble”, “tidyr”, “quantmod”, “rvest”, “dplyr”, “glue”))
library(tidyquant) # Para descargar los datos
library(plotly) # Para crear gr?ficos interactivos
library(timetk) # Para manipular las series de datos
library(tibble) #Para crear y manejar data frames modernos
library(tidyr) #Para organizar y transformar datos
library(forcats) #Para manipular variables categ?ricas (factores) de forma eficiente.
library(dplyr) #Para manipula datos: filtrar, resumir, transformar, ordenar, agrupar.
library(tidyverse) #Es un conjunto de paquetes integrados para an?lisis de datos limpio y estructurado.
library(quantmod) #Para descargar, analizar y visualizar datos financieros y series de tiempo del mercado.
library(rvest) #Sirve para hacer web scraping (extraer datos de páginas web HTML de forma estructurada)
library(glue) #Sirve para crear strings dinámicos fácilmente, insertando variables dentro de texto
library(stringr) #Sirve para manipular cadenas de texto (buscar, reemplazar, dividir, extraer patrones).
library(ggplot2) #Sirve para crear gráficos elegantes y personalizables con una sintaxis coherente basada en capas.
options(scipen=999)
set.seed(1234)
Luego procedemos a descargar los precios historicos de las acciones NU, ONON y NVDA, utilizando datos recopilados desde el 01/06/2022 al 06/04/2025
tick <- c('NU','ONON','NVDA')
price_data <- tq_get(tick,from='2022-06-01',to='2025-04-06',get='stock.prices')
head(price_data)
## # A tibble: 6 × 8
## symbol date open high low close volume adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 NU 2022-06-01 3.84 3.95 3.66 3.82 31535600 3.82
## 2 NU 2022-06-02 3.82 4.5 3.80 4.44 79498000 4.44
## 3 NU 2022-06-03 4.31 4.54 4.22 4.5 54708600 4.5
## 4 NU 2022-06-06 4.60 4.75 4.26 4.29 24604400 4.29
## 5 NU 2022-06-07 4.07 4.32 4 4.32 31965900 4.32
## 6 NU 2022-06-08 4.23 4.52 4.23 4.33 17304400 4.33
Para calcular el rendimiento diario de estas acciones
log_ret_tidy <- price_data%>%group_by(symbol)%>%tq_transmute(select=adjusted,mutate_fun=periodReturn,period='daily',col_rename='ret',type='log')
head(log_ret_tidy)
## # A tibble: 6 × 3
## # Groups: symbol [1]
## symbol date ret
## <chr> <date> <dbl>
## 1 NU 2022-06-01 0
## 2 NU 2022-06-02 0.150
## 3 NU 2022-06-03 0.0134
## 4 NU 2022-06-06 -0.0478
## 5 NU 2022-06-07 0.00697
## 6 NU 2022-06-08 0.00231
lo convertimos a un formato ancho y en un objeto de serie temporal
log_ret_xts <- log_ret_tidy%>%tidyr::pivot_wider(names_from=symbol, values_from=ret)%>%tk_xts()
## Warning: Non-numeric columns being dropped: date
## Using column `date` for date_var.
head(log_ret_xts)
## NU ONON NVDA
## 2022-06-01 0.000000000 0.000000000 0.000000000
## 2022-06-02 0.150403984 0.071262994 0.067127945
## 2022-06-03 0.013423007 -0.034540346 -0.045528668
## 2022-06-06 -0.047790673 -0.045281916 0.003519312
## 2022-06-07 0.006968718 0.010451369 0.007424809
## 2022-06-08 0.002312082 0.002831501 -0.014586369
A continuación, calculamos los rendimientos diarios medios de cada activo.
mean_ret <- colMeans(log_ret_xts)
print(round(mean_ret, 5))
## NU ONON NVDA
## 0.00129 0.00088 0.00230
Para calcular la matriz de covarianza para las 3 acciones; La anualizaremos multiplicandola por 252.
cov_mat <- cov(log_ret_xts) * 252
print(round(cov_mat,4))
## NU ONON NVDA
## NU 0.2813 0.0908 0.1089
## ONON 0.0908 0.2727 0.1072
## NVDA 0.1089 0.1072 0.2906
Antes de aplicar nuestros métodos a miles de carteras aleatorias, demostramos los pasos en una sola cartera. Para calcular la rentabilidad y el riesgo (desviación estándar) de la cartera, necesitaremos:
-Rendimiento medio de los activos -Ponderaciones de la cartera -Matriz de covarianza de todos los activos -Ponderaciones aleatorias
Primero, vamos a crear ponderaciones aleatorias
wts <- runif(n=length(tick))
wts <- wts/sum(wts)
A continuación calculamos la rentabilidad anualizada de la cartera.
port_returns <- (sum(wts*mean_ret)+1)^252-1
además, calculamos el riesgo de la cartera (desviación estándar). Esta será la desviación estándar anualizada de la cartera.
port_risk <- sqrt(t(wts)%*%(cov_mat%*%wts))
Finalemente calculamos el ratio de Sharpe
sharpe_ratio <- port_returns/port_risk
print(wts)
## [1] 0.08452041 0.46258068 0.45289891
print(port_returns)
## [1] 0.4799842
print(port_risk)
## [,1]
## [1,] 0.4246365
print(sharpe_ratio)
## [,1]
## [1,] 1.130341
Tenemos todo lo necesario para realizar la optimización. Ahora solo necesitamos ejecutar este código en 5000 carteras aleatorias. Para ello, usaremos un bucle llamado for. Antes de hacerlo, necesitamos crear vectores y una matriz de vacíos para almacenar nuestros valores.
num_port <- 5000
Creamos una matriz para almacenar las ponderaciones
all_wts <- matrix(nrow =num_port,ncol=length(tick))
Creamos un vector vacío para almacenar Rendimientos de la cartera
port_returns <- vector('numeric',length=num_port)
Creamos un vector vacío para almacenar Desviación estándar de la cartera
port_risk <- vector('numeric',length=num_port)
Creamos un vector vacío para almacenar índice de Sharpe de portafolio
sharpe_ratio <- vector('numeric',length=num_port)
Procedemos a ejecutar el bucle [for] 5000 veces.
for (i in seq_along(port_returns)) {
wts <- runif(length(tick))
wts <- wts/sum(wts)
# Almacenar ponderaci?n en la matriz
all_wts[i,] <- wts
# Rendimientos de la cartera
port_ret <- sum(wts*mean_ret)
port_ret <- ((port_ret+1)^252)-1
# Almacenamiento de valores de retorno de cartera
port_returns[i] <- port_ret
# Creaci?n y almacenamiento de riesgos de cartera
port_sd <- sqrt(t(wts)%*%(cov_mat%*%wts))
port_risk[i] <- port_sd
# Creaci?n y almacenamiento de ratios de Sharpe de cartera
# Suponiendo una tasa libre de riesgo del 0 %
sr <- port_ret/port_sd
sharpe_ratio[i] <- sr
}
Ahora creamos una tabla de datos para almacenar todos los valores juntos.
portfolio_values <- tibble(Return=port_returns,Risk=port_risk,SharpeRatio=sharpe_ratio)
Convertimos una matriz en un tibble y cambiamos los nombres de las columnas
all_wts <- tk_tbl(all_wts)
## Warning in tk_tbl.data.frame(as.data.frame(data), preserve_index, rename_index,
## : Warning: No index to preserve. Object otherwise converted to tibble
## successfully.
colnames(all_wts) <- colnames(log_ret_xts)
Combinamos todos los valores juntos
portfolio_values <- tk_tbl(cbind(all_wts, portfolio_values))
## Warning in tk_tbl.data.frame(cbind(all_wts, portfolio_values)): Warning: No
## index to preserve. Object otherwise converted to tibble successfully.
Tenemos las ponderaciones de cada activo, el riesgo y la rentabilidad, junto con el ratio de Sharpe de cada cartera.
Aunque algunos activos tengan alta volatilidad individual, al combinarlos con otros que estén poco correlacionados, el riesgo total puede reducirse.
min_var <- portfolio_values[which.min(portfolio_values$Risk),]
Graficamos las ponderaciones de la cartera de mínima varianza
p <- min_var%>%
pivot_longer(cols=NU:NVDA, names_to="Asset", values_to="Weights")%>%
mutate(Asset=as.factor(Asset))%>%
ggplot(aes(x=fct_reorder(Asset, Weights), y=Weights, fill=Asset))+
geom_bar(stat='identity')+
theme_minimal()+
labs(x='Assets', y='Weights', title="Minimum Variance Portfolio Weights")+
scale_y_continuous(labels=scales::percent)
ggplotly(p)
Bajo el portafolio de minima varianza, asumiendo una posicion conservadora, el peso de los 3 activos seleccionados son 29.49% para NVDA, 34.24% para NU y de 36.26% para ONON.
Asignamos USD 1.000.000 según las ponderaciones Extraemos las ponderaciones de min_var
weights_row <- min_var %>% select(NU, ONON, NVDA)
Calculamos la asignación en dólares (1 millón)
allocation_row <- weights_row * 1e6
Creamos y mostramos la tabla usando [rbind]
min_var_allocation <- rbind(Weights = weights_row,AllocationUSD = allocation_row)
print(min_var_allocation)
## # A tibble: 2 × 3
## NU ONON NVDA
## * <dbl> <dbl> <dbl>
## 1 0.340 0.366 0.293
## 2 340497. 366031. 293473.
Traemos los últimos precios historicos
ultimos_precios <- price_data %>%
group_by(symbol) %>%
filter(date == max(date)) %>%
select(symbol, adjusted) %>%
rename(Precio_Final = adjusted)
Tabla resumen con cantidades iniciales
resumen_inicial <- tibble(
Activo = c("NU", "ONON", "NVDA"),USD_Asignado = as.numeric(allocation_row))%>%
left_join(ultimos_precios, by = c("Activo" = "symbol")) %>%
mutate(Cantidad_Acciones = floor(USD_Asignado / Precio_Final),Valor_Real = Cantidad_Acciones * Precio_Final)
Valor inicial del portafolio
valor_inicial <- sum(resumen_inicial$Valor_Real)
nu_data <- price_data %>%
filter(symbol == "NU") %>%
select(date, adjusted)
price_nu=nu_data[,2]
log_ret_nu <- coredata(log_ret_xts[, "NU"])[-1]
plot.ts(price_nu)
plot.ts(log_ret_nu)
factores iniciales
year1=252
s01=as.numeric(nu_data[NROW(nu_data),2])
T1=504
r1=exp(mean(log_ret_nu)*year1)-1
sigma1= as.numeric(sqrt(var(log_ret_nu)*year1))
N1=5000 #N?mero de simulaciones
dt1=1/year1
stx1=as.numeric(s01*exp(r1*(T1/year1)))
vec1=rep(s01,N1)
mb1=matrix(ncol=N1, nrow=T1)
mb1[1,]=vec1
for (i in 1:N1) {
for (t in 2:T1) {
mb1[t,i]=mb1[(t-1),i]*exp((r1-(0.5*(sigma1^2)))*(dt1)+sigma1*(dt1^(1/2))*qnorm(runif(1, min=0, max=1)))
}
}
validacion antes de grafico
print(paste("Último precio de NU:", s01))
## [1] "Último precio de NU: 9.60000038146973"
print(paste("Volatilidad anualizada (sigma1):", sigma1))
## [1] "Volatilidad anualizada (sigma1): 0.530790771707883"
print(paste("Rendimiento anual esperado (r1):", r1))
## [1] "Rendimiento anual esperado (r1): 0.384994190335734"
final_prices <- mb1[nrow(mb1), ]
quantile(final_prices, probs = c(0.01, 0.05, 0.5, 0.95, 0.99))
## 1% 5% 50% 95% 99%
## 2.789032 4.585439 15.578939 51.583031 84.813421
max_value <- max(mb1)
print(max_value)
## [1] 354.8852
which_max <- which(mb1 == max_value, arr.ind = TRUE)
print(which_max)
## row col
## [1,] 498 1278
Dado que bajo el Movimiento Browniano Geométrico (MBG) los precios se simulan de forma exponencial, se observa que aproximadamente el 1% de las trayectorias presentan comportamientos explosivos. Esto se debe a la combinacion de un retorno anual esperado (drift) y una volatilidad anualizada relativamente altos, lo cual amplifica el crecimiento acumulado en el tiempo.
Estos valores extremos distorsionan el analisis general de la distribucion de precios simulados. Por esta razon, se decide excluir las trayectorias por encima del percentil 99% para representar de forma mas realista el comportamiento esperado del precio.
Extrae los precios finales
final_prices <- mb1[nrow(mb1), ]
Umbral del percentil 99
p99 <- quantile(final_prices, 0.99)
Filtramos trayectorias que terminan debajo del p99
filtered_mb1 <- mb1[, final_prices <= p99]
Calculamos percentiles a lo largo del tiempo
p_mat_nu <- apply(filtered_mb1, 1, function(x) quantile(x, probs = c(0.05, 0.5, 0.95)))
Transponemos para graficar
p_mat_nu <- t(p_mat_nu)
Grafica de trayectorias filtradas
matplot(filtered_mb1[, 1:100], type = "l", col = "gray", lty = 1,
main = "Simulaciones NU (trayectorias filtradas)", ylab = "Precio", xlab = "Días")
# Agrega lineas de percentiles
lines(p_mat_nu[, 1], col = "blue", lwd = 2, lty = 2) # P5
lines(p_mat_nu[, 2], col = "black", lwd = 2) # P50 (mediana)
lines(p_mat_nu[, 3], col = "red", lwd = 2, lty = 2) # P95
legend("topleft", legend = c("P5", "P50", "P95"),
col = c("blue", "black", "red"), lty = c(2,1,2), lwd = 2, bty = "n")
P5 (azul punteado): Muestra el límite inferior de los precios simulados, interpretado como una zona de riesgo de pérdida significativa. Es decir, un 5% de los caminos se encuentran por debajo de esa línea en cada día. Este trazo representa el VaR visual diario. P50 (negro sólido): Representa la mediana de las simulaciones. Es la mejor estimación no sesgada del precio futuro. Da una visión realista del comportamiento central. P95 (rojo punteado): Es el límite superior, marcando el rango superior del 5% más optimista. Ideal para ver zonas potenciales de ganancia extraordinaria.
onon_data <- price_data %>%
filter(symbol == "ONON") %>%
select(date, adjusted)
price_onon=onon_data[,2]
log_ret_onon <- coredata(log_ret_xts[, "ONON"])[-1]
plot.ts(price_onon)
plot.ts(log_ret_onon)
factores iniciales
year2=252
s02=as.numeric(onon_data[NROW(onon_data),2])
T2=504
r2=exp(mean(log_ret_onon)*year2)-1
sigma2= as.numeric(sqrt(var(log_ret_onon)*year2))
N2=5000 #numero de simulaciones
dt2=1/year2
stx2=as.numeric(s02*exp(r2*(T2/year2)))
vec2=rep(s02,N2)
mb2=matrix(ncol=N2, nrow=T2)
mb2[1,]=vec2
for (j in 1:N2) {
for (u in 2:T2) {
mb2[u,j]=mb2[(u-1),j]*exp((r2-(0.5*(sigma2^2)))*(dt2)+sigma2*(dt2^(1/2))*qnorm(runif(1, min=0, max=1)))
}
}
validación antes de grafico
print(paste("ultimo precio de ONON:", s02))
## [1] "ultimo precio de ONON: 39.6100006103516"
print(paste("Volatilidad anualizada (sigma2):", sigma2))
## [1] "Volatilidad anualizada (sigma2): 0.522581747655572"
print(paste("Rendimiento anual esperado (r2):", r2))
## [1] "Rendimiento anual esperado (r2): 0.248898893896643"
final_prices2 <- mb2[nrow(mb2), ]
quantile(final_prices2, probs = c(0.01, 0.05, 0.5, 0.95, 0.99))
## 1% 5% 50% 95% 99%
## 9.065471 14.491122 49.448533 168.537093 277.576746
max_value2 <- max(mb2)
print(max_value2)
## [1] 902.331
which_max2 <- which(mb2 == max_value2, arr.ind = TRUE)
print(which_max2)
## row col
## [1,] 504 2667
###ANALISIS: Dado que bajo el Movimiento Browniano Geométrico (MBG) los precios se simulan de forma exponencial, se observa que aproximadamente el 5% de las trayectorias presentan comportamientos explosivos. Esto se debe a la combinacion de un retorno anual esperado (drift) y una volatilidad anualizada relativamente altos, lo cual amplifica el crecimiento acumulado en el tiempo.
Estos valores extremos distorsionan el analisis general de la distribucion de precios simulados. Por esta razon, se decide excluir las trayectorias por encima del percentil 95% para representar de forma mas realista el comportamiento esperado del precio.
Extraemos los precios finales
final_prices2 <- mb2[nrow(mb2), ]
Umbral del percentil 95
p95 <- quantile(final_prices2, 0.95)
Filtramos trayectorias que terminan debajo del p95
filtered_mb2 <- mb2[, final_prices2 <= p95]
Calculamos percentiles a lo largo del tiempo
p_mat_onon <- apply(filtered_mb2, 1, function(x) quantile(x, probs = c(0.05, 0.5, 0.95)))
Transponemos para graficar
p_mat_onon <- t(p_mat_onon)
Graficamos las trayectorias filtradas
matplot(filtered_mb2[, 1:100], type = "l", col = "gray", lty = 1,
main = "Simulaciones ONON (trayectorias filtradas)", ylab = "Precio", xlab = "Dias")
# Agrega lineas de percentiles
lines(p_mat_onon[, 1], col = "blue", lwd = 2, lty = 2) # P5
lines(p_mat_onon[, 2], col = "black", lwd = 2) # P50 (mediana)
lines(p_mat_onon[, 3], col = "red", lwd = 2, lty = 2) # P95
legend("topleft", legend = c("P5", "P50", "P95"),
col = c("blue", "black", "red"), lty = c(2,1,2), lwd = 2, bty = "n")
P5 (azul punteado): Muestra el límite inferior de los precios simulados, interpretado como una zona de riesgo de pérdida significativa. Es decir, un 5% de los caminos se encuentran por debajo de esa línea en cada día. Este trazo representa el VaR visual diario. P50 (negro sólido): Representa la mediana de las simulaciones. Es la mejor estimación no sesgada del precio futuro. Da una visión realista del comportamiento central. P95 (rojo punteado): Es el límite superior, marcando el rango superior del 5% más optimista. Ideal para ver zonas potenciales de ganancia extraordinaria.
nvda_data <- price_data %>%
filter(symbol == "NVDA") %>%
select(date, adjusted)
price_nvda=nvda_data[,2]
log_ret_nvda <- coredata(log_ret_xts[, "NVDA"])[-1]
plot.ts(price_nvda)
plot.ts(log_ret_nvda)
factores iniciales
year3=252
s03=as.numeric(nvda_data[NROW(nvda_data),2])
T3=504
r3=exp(mean(log_ret_nvda)*year3)-1
sigma3=as.numeric(sqrt(var(log_ret_nvda)*year3))
r3 <- 0.4
N3=5000 #numero de simulaciones
dt3=1/year3
stx3=as.numeric(s03*exp(r3*(T3/year3)))
vec3=rep(s03,N3)
mb3=matrix(ncol=N3, nrow=T3)
mb3[1,]=vec3
for (K in 1:N3) {
for (V in 2:T3) {
mb3[V,K]=mb3[(V-1),K]*exp((r3-(0.5*(sigma3^2)))*(dt3)+sigma3*(dt3^(1/2))*qnorm(runif(1, min=0, max=1)))
}
}
validación antes de grafico
print(paste("Ultimo precio de NVDA:", s03))
## [1] "Ultimo precio de NVDA: 94.3099975585938"
print(paste("Volatilidad anualizada (sigma3):", sigma3))
## [1] "Volatilidad anualizada (sigma3): 0.539449779245821"
print(paste("Rendimiento anual esperado (r3):", r3))
## [1] "Rendimiento anual esperado (r3): 0.4"
final_prices3 <- mb3[nrow(mb3), ]
quantile(final_prices3, probs = c(0.01, 0.05, 0.5, 0.95, 0.99))
## 1% 5% 50% 95% 99%
## 26.2168 45.3470 153.7164 534.8470 931.4145
max_value3 <- max(mb3)
print(max_value3)
## [1] 2585.748
which_max3 <- which(mb3 == max_value3, arr.ind = TRUE)
print(which_max3)
## row col
## [1,] 469 546
###ANALISIS: Dado que bajo el Movimiento Browniano Geométrico (MBG) los precios se simulan de forma exponencial, se observa que aproximadamente el 5% de las trayectorias presentan comportamientos explosivos. Esto se debe a la combinacion de un retorno anual esperado (drift) y una volatilidad anualizada relativamente altos, lo cual amplifica el crecimiento acumulado en el tiempo.
Estos valores extremos distorsionan el analisis general de la distribucion de precios simulados. Por esta razon, se decide excluir las trayectorias por encima del percentil 95% para representar de forma mas realista el comportamiento esperado del precio, adicional para fines academicos se reducira volatilidad anualizada a la mitad.
Extraemos los precios finales
final_prices3 <- mb3[nrow(mb3), ]
Umbral del percentil 95
p95_2 <- quantile(final_prices3, 0.95)
Filtramos trayectorias que terminan debajo del p95
filtered_mb3 <- mb3[, final_prices3 <= p95_2]
Calculamos percentiles a lo largo del tiempo
p_mat_nvda <- apply(filtered_mb3, 1, function(x) quantile(x, probs = c(0.05, 0.5, 0.95)))
Transponemos para graficar
p_mat_nvda <- t(p_mat_nvda)
Grafica de las trayectorias filtradas
matplot(filtered_mb3[, 1:100], type = "l", col = "gray", lty = 1,
main = "Simulaciones NVDA (trayectorias filtradas)", ylab = "Precio", xlab = "Dias")
# Agrega lineas de percentiles
lines(p_mat_nvda[, 1], col = "blue", lwd = 2, lty = 2) # P5
lines(p_mat_nvda[, 2], col = "black", lwd = 2) # P50 (mediana)
lines(p_mat_nvda[, 3], col = "red", lwd = 2, lty = 2) # P95
legend("topleft", legend = c("P5", "P50", "P95"),
col = c("blue", "black", "red"), lty = c(2,1,2), lwd = 2, bty = "n")
P5 (azul punteado): Muestra el límite inferior de los precios simulados, interpretado como una zona de riesgo de pérdida significativa. Es decir, un 5% de los caminos se encuentran por debajo de esa línea en cada día. Este trazo representa el VaR visual diario. P50 (negro sólido): Representa la mediana de las simulaciones. Es la mejor estimación no sesgada del precio futuro. Da una visión realista del comportamiento central. P95 (rojo punteado): Es el límite superior, marcando el rango superior del 5% más optimista. Ideal para ver zonas potenciales de ganancia extraordinaria.
Aplicamos una función para seleccionar una trayectoria decreciente
elegir_trayectoria_decreciente <- function(sim_matrix) {
indices <- which(sim_matrix[nrow(sim_matrix), ] < sim_matrix[1, ])
if (length(indices) == 0) stop("No hay trayectorias en caida")
sim_matrix[, sample(indices, 1)]
}
Elegimos trayectorias en caida
trayectoria_NU <- elegir_trayectoria_decreciente(mb1)
trayectoria_ONON <- elegir_trayectoria_decreciente(mb2)
trayectoria_NVDA <- elegir_trayectoria_decreciente(mb3)
Seguimiento trismestral del portafolio Dias a observar: cada 63 dias desde el inicio hasta 504 (8 trimestres + dia inicial)
dias_seguimiento <- c(1, seq(63, 504, by = 63))
Extraemos cantidades para cada acción
cantidad_NU <- resumen_inicial %>% filter(Activo == "NU") %>% pull(Cantidad_Acciones)
cantidad_ONON <- resumen_inicial %>% filter(Activo == "ONON") %>% pull(Cantidad_Acciones)
cantidad_NVDA <- resumen_inicial %>% filter(Activo == "NVDA") %>% pull(Cantidad_Acciones)
Creamos la tabla para registrar evolución del portafolio
seguimiento_portafolio <- tibble(
Trimestre = 0:(length(dias_seguimiento) - 1),
Dia_Simulacion = dias_seguimiento,
Cantidad_NU = cantidad_NU,
Cantidad_ONON = cantidad_ONON,
Cantidad_NVDA = cantidad_NVDA,
Precio_NU = trayectoria_NU[dias_seguimiento],
Precio_ONON = trayectoria_ONON[dias_seguimiento],
Precio_NVDA = trayectoria_NVDA[dias_seguimiento],
Valor_NU = Precio_NU * cantidad_NU,
Valor_ONON = Precio_ONON * cantidad_ONON,
Valor_NVDA = Precio_NVDA * cantidad_NVDA,
Valor_Portafolio = Valor_NU + Valor_ONON + Valor_NVDA,
Diferencia_Valor = Valor_Portafolio - valor_inicial,
)
Descargamos la tasa libre de riesgo de FRED (3-Month Treasury Bill: ^IRX o DGS3MO)
getSymbols("DGS3MO", src = "FRED") # Tasa a 3 meses
## [1] "DGS3MO"
rf_data <- na.omit(DGS3MO)
rf_trimestral <- tail(as.numeric(rf_data), 1) / 100 # Ultimo valor y lo llevamos a decimal
Calculamos retornos del portafolio (log-return trimestral)
seguimiento_portafolio <- seguimiento_portafolio %>%
mutate(
Retorno = c(NA, diff(log(Valor_Portafolio))),
Volatilidad = zoo::rollapply(Retorno, width = 2, FUN = sd, fill = NA, align = "right"),
Sharpe = ifelse(!is.na(Retorno), (Retorno - (rf_trimestral / 4)) / Volatilidad, NA)
)
print(seguimiento_portafolio)
## # A tibble: 9 × 16
## Trimestre Dia_Simulacion Cantidad_NU Cantidad_ONON Cantidad_NVDA Precio_NU
## <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 1 35468 9240 3111 9.60
## 2 1 63 35468 9240 3111 9.83
## 3 2 126 35468 9240 3111 6.72
## 4 3 189 35468 9240 3111 5.09
## 5 4 252 35468 9240 3111 4.16
## 6 5 315 35468 9240 3111 2.40
## 7 6 378 35468 9240 3111 2.82
## 8 7 441 35468 9240 3111 2.62
## 9 8 504 35468 9240 3111 2.90
## # ℹ 10 more variables: Precio_ONON <dbl>, Precio_NVDA <dbl>, Valor_NU <dbl>,
## # Valor_ONON <dbl>, Valor_NVDA <dbl>, Valor_Portafolio <dbl>,
## # Diferencia_Valor <dbl>, Retorno <dbl>, Volatilidad <dbl>, Sharpe <dbl>
Calculamos VaR historico (al 1% y 5%) basado en retorno Se usa rolling de 2 para cada trimestre
seguimiento_portafolio <- seguimiento_portafolio %>%
mutate(
VaR_1 = zoo::rollapply(Retorno, width = 2, FUN = function(x) quantile(x, 0.01, na.rm = TRUE), fill = NA, align = "right"),
VaR_5 = zoo::rollapply(Retorno, width = 2, FUN = function(x) quantile(x, 0.05, na.rm = TRUE), fill = NA, align = "right"),
VaR_1 = pmin(VaR_1, 0),
VaR_5 = pmin(VaR_5, 0),
VVaR_1_USD = VaR_1 * lag(Valor_Portafolio),
VaR_5_USD = VaR_5 * lag(Valor_Portafolio)
)
Resultado final con todas las metricas por trimestre
print(seguimiento_portafolio)
## # A tibble: 9 × 20
## Trimestre Dia_Simulacion Cantidad_NU Cantidad_ONON Cantidad_NVDA Precio_NU
## <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 1 35468 9240 3111 9.60
## 2 1 63 35468 9240 3111 9.83
## 3 2 126 35468 9240 3111 6.72
## 4 3 189 35468 9240 3111 5.09
## 5 4 252 35468 9240 3111 4.16
## 6 5 315 35468 9240 3111 2.40
## 7 6 378 35468 9240 3111 2.82
## 8 7 441 35468 9240 3111 2.62
## 9 8 504 35468 9240 3111 2.90
## # ℹ 14 more variables: Precio_ONON <dbl>, Precio_NVDA <dbl>, Valor_NU <dbl>,
## # Valor_ONON <dbl>, Valor_NVDA <dbl>, Valor_Portafolio <dbl>,
## # Diferencia_Valor <dbl>, Retorno <dbl>, Volatilidad <dbl>, Sharpe <dbl>,
## # VaR_1 <dbl>, VaR_5 <dbl>, VVaR_1_USD <dbl>, VaR_5_USD <dbl>
Ya que las opciones son una herramienta poderosa para cubrirse de los riesgos asociados, especialmente si el mercado es volátil o incierto, como en nuestro caso donde las acciones tienen una tendencia bajista, si bien no eliminan completamente el riesgo, proporcionan una forma efectiva de gestionarlo, lo que puede ser crucial para evitar pérdidas significativas. Sin embargo, es importante recordar que las opciones también implican costos (como la prima de la opción), por lo tanto se debe analizar cuales opciones realmente nos ayudan a compensar más las pérdidas.
Para nuestro análisis de cobertura con opciones elegimos realizarlo unicamente con NVDA ya que evaluando los valores de las opciones, y sus primas en el mercado para las 3 acciones elegidas (NVDA-NU-ONON), pudimos encontrar que NVDA presente una alta liquidez, multiples estrategias disponibles y un bajo costo; por el contrario para NU encontramos algunas opciones viables pero con spreads grandes; asi como para ONON no encontramos coberturas recomendables ya que hay pocos strikes activos.
Primero procedemos a definir un precio Strike
Precio actual de NVDA (ya calculado previamente)
nvda_price <- s03
URL de opciones para NVDA
url <- "https://finance.yahoo.com/quote/NVDA/options?p=NVDA"
Leer la página con cabecera de navegador (para evitar bloqueos)
page <- read_html(httr::GET(url, httr::add_headers(`User-Agent` = "Mozilla/5.0")))
Extraer la tabla de PUTs (segunda tabla)
puts_table <- page %>%
html_elements("table") %>%
.[[2]] %>%
html_table(fill = TRUE)
Limpiar nombres de columnas
names(puts_table) <- gsub("\\s+", "_", names(puts_table))
Convertir columnas clave y calcular el spread
puts_clean <- puts_table %>%
mutate(
Strike = as.numeric(Strike),
Bid = as.numeric(Bid),
Ask = as.numeric(Ask),
Last_Price = as.numeric(Last_Price),
Implied_Volatility = as.numeric(gsub("%", "", Implied_Volatility)) / 100,
spread = Ask - Bid
) %>%
filter(
!is.na(Strike), !is.na(Bid), !is.na(Ask),
Strike > nvda_price # <-- solo ITM
) %>%
arrange(spread)
Convertimos también Open Interest (por si es character con comas)
puts_clean <- puts_clean %>%
mutate(
Open_Interest = as.numeric(gsub(",", "", Open_Interest)),
# Score que premia alto interés abierto y bajo spread
score = Open_Interest / (spread + 1e-6) # evitamos división por 0
) %>%
arrange(desc(score)) # Mejor score = mayor liquidez y menor spread
Seleccionamos la mejor opción
best_put <- puts_clean %>% slice(1)
selected_strike <- best_put$Strike
selected_prima <- best_put$Last_Price
Mostramos resultados
glue::glue("Strike seleccionado (PUT ITM con alto interés y bajo spread): {selected_strike}")
## Strike seleccionado (PUT ITM con alto interés y bajo spread): 101
print(best_put)
## # A tibble: 1 × 13
## Contract_Name Last_Trade_Date_(EDT…¹ Strike Last_Price Bid Ask Change
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 NVDA250411P001010… 4/4/2025 3:59 PM 101 8.25 6.55 6.55 5
## # ℹ abbreviated name: ¹`Last_Trade_Date_(EDT)`
## # ℹ 6 more variables: `%_Change` <chr>, Volume <chr>, Open_Interest <dbl>,
## # Implied_Volatility <dbl>, spread <dbl>, score <dbl>
PARÁMETROS INICIALES
precio_inicial <- s03 # Precio actual de NVDA
strike <- selected_strike # Precio de ejercicio (strike)
horizonte <- 2 # Horizonte de tiempo (años)
pasos <- 8 # Número de pasos (8 trimestres)
volatilidad <- sigma3 # Volatilidad anual
tasa_rf <- rf_trimestral # Tasa libre de riesgo anual
CÁLCULOS BASE
delta_t <- horizonte / pasos
sube <- exp(volatilidad * sqrt(delta_t))
baja <- 1 / sube
probabilidad <- (exp(tasa_rf * delta_t) - baja) / (sube - baja)
Árbol de precios del subyacente
precios_arbol <- matrix(0, nrow = pasos+1, ncol = pasos+1)
for (columna in 0:pasos) {
for (fila in 0:columna) {
precios_arbol[fila+1, columna+1] <- precio_inicial * sube^(columna-fila) * baja^fila
}
}
Árbol de valoración de la put
valor_opcion <- matrix(0, nrow = pasos+1, ncol = pasos+1)
valor_opcion[, pasos+1] <- pmax(strike - precios_arbol[, pasos+1], 0)
Valoración hacia atrás
for (columna in (pasos-1):0) {
for (fila in 0:columna) {
valor_opcion[fila+1, columna+1] <- exp(-tasa_rf * delta_t) * (
probabilidad * valor_opcion[fila+1, columna+2] +
(1 - probabilidad) * valor_opcion[fila+2, columna+2]
)
}
}
Preparamos DataFrame para graficar
datos_grafico <- data.frame()
for (columna in 0:pasos) {
for (fila in 0:columna) {
datos_grafico <- rbind(datos_grafico, data.frame(
Paso = columna,
Nodo = fila,
Precio = precios_arbol[fila+1, columna+1],
Valor = round(valor_opcion[fila+1, columna+1], 2)
))
}
}
Ajustamos posiciones para graficar tipo árbol
datos_grafico <- datos_grafico %>%
mutate(x = Paso,
y = -Nodo + Paso / 2)
Graficamos árbol binomial
ggplot(datos_grafico, aes(x = x, y = y)) +
geom_point(aes(color = Valor > 0), size = 5) +
geom_text(aes(label = paste0("S=", round(Precio,1), "\nP=", Valor)), size = 3) +
scale_color_manual(values = c("FALSE" = "gray70", "TRUE" = "red")) +
labs(title = "Árbol Binomial de una Put Europea sobre NVDA",
subtitle = "Precios del activo y valor de la opción en cada nodo",
x = "Paso (Trimestre)", y = "Nodo") +
theme_minimal() +
theme(legend.position = "none")
Parámetros
porcentaje_cubierto <- 0.85
strike_price <- selected_strike # Asumimos que ya está definida
num_opciones <- ceiling(porcentaje_cubierto * seguimiento_portafolio$Cantidad_NVDA[1] / 100)
costo_prima <- num_opciones * selected_prima * 100
Función para encontrar valor de opción desde el árbol en un trimestre y precio dado
obtener_valor_opcion_desde_arbol <- function(trimestre, precio_simulado, precios_arbol, valor_opcion) {
if (trimestre == 0) return(0) # No hay cobertura en t=0
# Extraer precios y valores para el trimestre correspondiente
precios_trimestre <- precios_arbol[1:(trimestre+1), trimestre+1]
valores_trimestre <- valor_opcion[1:(trimestre+1), trimestre+1]
# Buscar el nodo más cercano al precio simulado
idx_cercano <- which.min(abs(precios_trimestre - precio_simulado))
return(valores_trimestre[idx_cercano])
}
Aplicamos la función por cada fila
seguimiento_portafolio$Valor_Put <- mapply(
obtener_valor_opcion_desde_arbol,
seguimiento_portafolio$Trimestre,
seguimiento_portafolio$Precio_NVDA,
MoreArgs = list(precios_arbol = precios_arbol, valor_opcion = valor_opcion)
)
Calculamos el ingreso por cobertura (valor PUT * 100 * opciones) por trimestre
seguimiento_portafolio$Ingreso_Cobertura_NVDA <- seguimiento_portafolio$Valor_Put * num_opciones * 100 - costo_prima
seguimiento_portafolio$Ingreso_Cobertura_NVDA[1]=0
seguimiento_portafolio$Costo_Prima[1]=0
## Warning: Unknown or uninitialised column: `Costo_Prima`.
Asi como también encontramos el valor del portafolio con cobertura
seguimiento_portafolio$Valor_Portafolio_Cubierto <- seguimiento_portafolio$Valor_Portafolio +
seguimiento_portafolio$Ingreso_Cobertura_NVDA - cumsum(seguimiento_portafolio$Costo_Prima)
Las Diferencias respecto al valor inicial
valor_inicial <- seguimiento_portafolio$Valor_Portafolio[1]
seguimiento_portafolio$Diferencia_Sin_Cobertura <- seguimiento_portafolio$Valor_Portafolio - valor_inicial
seguimiento_portafolio$Diferencia_Con_Cobertura <- seguimiento_portafolio$Valor_Portafolio_Cubierto - valor_inicial
Convertimos el dataframe a formato largo para ggplot
df_plot <- seguimiento_portafolio[, c("Trimestre", "Valor_Portafolio", "Valor_Portafolio_Cubierto")]
df_plot <- reshape2::melt(df_plot, id.vars = "Trimestre")
Cambiar nombres para presentación
df_plot$variable <- factor(df_plot$variable,
levels = c("Valor_Portafolio", "Valor_Portafolio_Cubierto"),
labels = c("Sin Cobertura", "Con Cobertura"))
Graficar
ggplot(df_plot, aes(x = factor(Trimestre), y = value, fill = variable)) +
geom_bar(stat = "identity", position = "dodge", color = "black") +
labs(
title = "Valor del Portafolio por Trimestre: Con y Sin Cobertura",
x = "Trimestre",
y = "Valor del Portafolio (USD)",
fill = "Estrategia"
) +
scale_fill_manual(values = c("steelblue", "darkorange")) +
theme_minimal(base_size = 14) +
theme(
legend.position = "top",
plot.title = element_text(face = "bold", hjust = 0.5)
) +
geom_text(aes(label = round(value, 0)),
position = position_dodge(width = 0.9),
vjust = -0.5, size = 4)
A partir del trimestre 1 en adelante, el portafolio con cobertura mantiene consistentemente un valor mayor que el portafolio sin cobertura, lo que indica que la estrategia de cobertura si es efectiva para mitigar las pérdidas, especialmente durante los trimestres en que el valor del portafolio cae más fuertemente como en los trimestres 5 y 6. Asi mismo,la cobertura contribuye a reducir la volatilidad del portafolio, proporcionando mayor estabilidad en escenarios adversos.
Finalmente, esta gráfica justifica el uso de estrategias de cobertura dinámica, especialmente en portafolios expuestos a activos volátiles como acciones tecnológicas, para nuestra caso NVIDIA. Además, se evidencia que cuando aplicamos la cobertura mejoramos el perfil riesgo-retorno, ya que se respaldar cuantitativamente con indicadores como el índice de Sharpe y el VaR.