#Instalamos los respectivos paquetes para correr todos los codigos
library(readxl)
library(dplyr)
##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidyr)
library(ggplot2)
library(tseries)
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
#Leemos el archivo con los datos sacados de Bloomberg, con sus respectivas acciones a 10 años
Acciones_raw <- read_excel("C:/Users/User/OneDrive/Escritorio/acciones.xlsx")
## New names:
## • `` -> `...2`
## • `` -> `...3`
## • `` -> `...4`
## • `` -> `...5`
## • `` -> `...7`
## • `` -> `...8`
## • `` -> `...9`
## • `` -> `...10`
## • `` -> `...12`
## • `` -> `...13`
## • `` -> `...14`
TXN – Sector tecnológico. Expectativas positivas por expansión en
semiconductores.
PEP – Sector consumo. Estabilidad y flujo de caja constante.
UPS – Sector transporte y logística. Recuperación post-pandemia y mejora
en márgenes.
Estas empresas presentan diversificación sectorial y volatilidades moderadas, ideales para conformar un portafolio equilibrado.
#Detectar las columnas con formato de fecha
is_excel_date_col <- function(x){
num <- suppressWarnings(as.numeric(x))
mean(!is.na(num) & num > 30000 & num < 50000) > 0.5
}
is_date <- sapply(Acciones_raw, is_excel_date_col)
date_idxs <- which(is_date)
pairs <- data.frame(date_idx = date_idxs, price_idx = date_idxs + 1)
pairs <- pairs[pairs$price_idx <= ncol(Acciones_raw), ]
pairs
## date_idx price_idx
## TXN 1 2
## PEP 6 7
## UPS 11 12
#Creamos la base de datos limpia solo con fechas y precios de cada acción
nm <- names(Acciones_raw)
list_tbls <- lapply(1:nrow(pairs), function(i){
di <- pairs$date_idx[i]
pi <- pairs$price_idx[i]
ticker_name <- nm[pi]
ticker_name <- gsub("[^A-Za-z0-9]", "", ticker_name)
dates_raw <- Acciones_raw[[di]]
prices_raw <- Acciones_raw[[pi]]
# Intentar convertir fechas (varios formatos posibles)
dates_conv <- suppressWarnings(as.Date(as.numeric(dates_raw), origin = "1899-12-30"))
if(all(is.na(dates_conv))){
dates_conv <- suppressWarnings(as.Date(as.character(dates_raw), format = "%Y-%m-%d"))
}
if(all(is.na(dates_conv))){
dates_conv <- suppressWarnings(as.Date(as.character(dates_raw), format = "%d/%m/%Y"))
}
prices_conv <- suppressWarnings(as.numeric(gsub(",", "", as.character(prices_raw))))
data.frame(Date = dates_conv, Price = prices_conv, Ticker = ticker_name)
})
# --- Corrección principal ---
# Unir todas las tablas y asegurar que sea data.frame limpio
df_long <- bind_rows(list_tbls)
df_wide <- df_long %>%
pivot_wider(names_from = Ticker, values_from = Price) %>%
arrange(Date)
# 🔧 Forzar a data.frame y eliminar atributos ts/mts
df_wide <- as.data.frame(df_wide)
attr(df_wide, "tsp") <- NULL
class(df_wide) <- setdiff(class(df_wide), c("ts", "mts"))
# Limpiar fechas vacías y duplicadas (usando dplyr de forma explícita)
Acciones <- df_wide %>%
dplyr::filter(!is.na(Date)) %>%
dplyr::distinct(Date, .keep_all = TRUE)
# Renombrar columnas (por claridad)
if (ncol(Acciones) >= 4) {
names(Acciones)[1:4] <- c("Date", "TXN", "PEP", "UPS")
}
head(Acciones)
## Date TXN PEP UPS
## 1 2014-08-29 48.180 92.49 97.33
## 2 2014-09-30 47.690 93.09 98.29
## 3 2014-10-31 49.660 96.17 104.91
## 4 2014-11-28 54.420 100.10 109.92
## 5 2014-12-31 53.465 94.56 111.17
## 6 2015-01-30 53.450 93.78 98.84
#Calcular retornos logarítmicos
retornos <- data.frame(
Date = Acciones$Date[-1],
ret_TXN = diff(log(Acciones$TXN)),
ret_PEP = diff(log(Acciones$PEP)),
ret_UPS = diff(log(Acciones$UPS))
)
head(retornos)
## Date ret_TXN ret_PEP ret_UPS
## 1 2014-09-30 -0.0102222649 0.006466237 0.009815026
## 2 2014-10-31 0.0404780478 0.032550692 0.065180547
## 3 2014-11-28 0.0915319533 0.040052228 0.046649989
## 4 2014-12-31 -0.0177044991 -0.056935133 0.011307733
## 5 2015-01-30 -0.0002805967 -0.008282940 -0.117558180
## 6 2015-02-27 0.0953952174 0.053966196 0.028819864
#Medias, desviaciones y matrices de covarianza/correlación
ret_mat <- as.matrix(retornos[, -1])
medias <- colMeans(ret_mat, na.rm = TRUE)
desv <- apply(ret_mat, 2, sd, na.rm = TRUE)
# Matrices
cov_mat <- cov(ret_mat, use = "complete.obs")
cor_mat <- cor(ret_mat, use = "complete.obs")
medias
## ret_TXN ret_PEP ret_UPS
## 9.272468e-03 3.614436e-03 -7.474693e-05
desv
## ret_TXN ret_PEP ret_UPS
## 0.06731444 0.04350410 0.07583505
cov_mat
## ret_TXN ret_PEP ret_UPS
## ret_TXN 0.004531234 0.001067204 0.002358746
## ret_PEP 0.001067204 0.001892607 0.001227548
## ret_UPS 0.002358746 0.001227548 0.005750955
cor_mat
## ret_TXN ret_PEP ret_UPS
## ret_TXN 1.0000000 0.3644258 0.4620648
## ret_PEP 0.3644258 1.0000000 0.3720816
## ret_UPS 0.4620648 0.3720816 1.0000000
##Valores mostrados en anual
medias_anual <- medias * 252
desv_anual <- desv * sqrt(252)
medias_anual
## ret_TXN ret_PEP ret_UPS
## 2.33666200 0.91083791 -0.01883623
desv_anual
## ret_TXN ret_PEP ret_UPS
## 1.0685837 0.6906062 1.2038441
#Portafolio de mínima varianza
opt <- portfolio.optim(ret_mat)
opt$pw # Pesos óptimos
## [1] 0.16043577 0.77140070 0.06816353
opt$exp.ret # Retorno esperado diario
## NULL
opt$ps # Desviación estándar diaria
## [1] 0.04140535
# Anualizar
ret_port_anual <- opt$exp.ret * 252
sd_port_anual <- opt$ps * sqrt(252)
ret_port_anual
## numeric(0)
sd_port_anual
## [1] 0.6572895
SPX <- read_excel("C:/Users/User/OneDrive/Escritorio/acciones.xlsx")
## New names:
## • `` -> `...2`
## • `` -> `...3`
## • `` -> `...4`
## • `` -> `...5`
## • `` -> `...7`
## • `` -> `...8`
## • `` -> `...9`
## • `` -> `...10`
## • `` -> `...12`
## • `` -> `...13`
## • `` -> `...14`
SPX <- SPX %>%
rename(Date = 1, Price = 2) %>%
mutate(
# Convertir fechas
Date = as.Date(as.numeric(Date), origin = "1899-12-30"),
# Limpiar comas o texto y convertir a número
Price = as.numeric(gsub(",", "", as.character(Price)))
) %>%
arrange(Date)
## Warning: There were 2 warnings in `mutate()`.
## The first warning was:
## ℹ In argument: `Date = as.Date(as.numeric(Date), origin = "1899-12-30")`.
## Caused by warning in `as.Date()`:
## ! NAs introducidos por coerción
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
head(SPX)
## # A tibble: 6 × 14
## Date Price ...3 ...4 ...5 PEP ...7 ...8 ...9 ...10 UPS ...12
## <date> <dbl> <chr> <chr> <lgl> <chr> <chr> <chr> <chr> <lgl> <chr> <chr>
## 1 2014-08-29 48.2 986000… <NA> NA 41880 92.49 9102… <NA> NA 41880 97.33
## 2 2014-09-30 47.7 989200… <NA> NA 41912 93.09 8690… <NA> NA 41912 98.29
## 3 2014-10-31 49.7 242770… <NA> NA 41943 96.17 1267… <NA> NA 41943 104.…
## 4 2014-11-28 54.4 947100… <NA> NA 41971 100.1 7541… <NA> NA 41971 109.…
## 5 2014-12-31 53.5 120990… <NA> NA 42004 94.56 9441… <NA> NA 42004 111.…
## 6 2015-01-30 53.4 117800… <NA> NA 42034 93.78 1122… <NA> NA 42034 98.84
## # ℹ 2 more variables: ...13 <chr>, ...14 <chr>
#Calculo de las Betas con el índice S&P500
SPX <- read_excel("C:/Users/User/OneDrive/Escritorio/acciones.xlsx")
## New names:
## • `` -> `...2`
## • `` -> `...3`
## • `` -> `...4`
## • `` -> `...5`
## • `` -> `...7`
## • `` -> `...8`
## • `` -> `...9`
## • `` -> `...10`
## • `` -> `...12`
## • `` -> `...13`
## • `` -> `...14`
SPX <- SPX %>%
rename(Date = 1, Price = 2) %>%
mutate(Date = as.Date(as.numeric(Date), origin = "1899-12-30")) %>%
arrange(Date)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `Date = as.Date(as.numeric(Date), origin = "1899-12-30")`.
## Caused by warning in `as.Date()`:
## ! NAs introducidos por coerción
SPX$Price <- as.numeric(gsub("[^0-9.]", "", as.character(SPX$Price)))
spx_ret <- data.frame(Date = SPX$Date[-1],
ret_SPX = diff(log(SPX$Price)))
# Unir con los retornos de las acciones
merged <- merge(retornos, spx_ret, by = "Date")
# Calcular betas
betas <- sapply(c("ret_TXN", "ret_PEP", "ret_UPS"), function(col){
coef(lm(as.formula(paste(col, "~ ret_SPX")), data = merged))[2]
})
betas
## ret_TXN.ret_SPX ret_PEP.ret_SPX ret_UPS.ret_SPX
## 1.0000000 0.2355218 0.5205526
#Betas del protafolio
beta_port <- sum(opt$pw * betas)
beta_port
## [1] 0.3776001
S_indice <- 5500.00
Q_futuro <- 250
C_portafolio <- 10000000
rf_anual <- 0.045
#Analisis de sensibilidad de betas
beta_alternativa_1 <- 0.8
beta_alternativa_2 <- 1.5
N_beta_0_8 <- (beta_alternativa_1 * C_portafolio) / (S_indice * Q_futuro)
N_beta_1_5 <- (beta_alternativa_2 * C_portafolio) / (S_indice * Q_futuro)
cat("\nAnalisis de Sensibilidad de Contratos:\n")
##
## Analisis de Sensibilidad de Contratos:
cat("Si Beta 0.8:", round(N_beta_0_8, 2), "contratos.\n")
## Si Beta 0.8: 5.82 contratos.
cat("Si Beta 1.5:", round(N_beta_1_5, 2), "contratos.\n")
## Si Beta 1.5: 10.91 contratos.
# Una Beta menor (0.8) requiere menos contratos, lo que implica una menor cobertura de riesgo sistemático.
# Una Beta mayor (1.5) requiere más contratos, lo que implica una mayor cobertura, pero también un mayor costo de margen.
#Calcular VaR histórico (5% y 1%)
ret_port <- as.numeric(ret_mat %*% opt$pw)
VaR_5 <- quantile(ret_port, probs = 0.05)
VaR_1 <- quantile(ret_port, probs = 0.01)
VaR_5
## 5%
## -0.06565905
VaR_1
## 1%
## -0.09499933
El Valor en Riesgo (VaR) mide la pérdida máxima que se espera para tu cartera en un día de negociación, bajo condiciones normales de mercado. VaR al 5% (-6.57%): Existe un 95% de probabilidad de que el portafolio no pierda más del 6.57% de su valor cada día. VaR al 1% (-9.50%): Aumentando la confianza, existe una probabilidad del 99% de que la pérdida diaria no exceda el 9.50% del valor del portafolio. Conclusión: Estos valores indican el capital que está en riesgo. Por ejemplo, si el portafolio tiene un valor de \(10,000,000 COP1\), el VaR al 5% sugiere que en 5 días por cada 100 la pérdida podría superar los $657,000 COP. Esta es la cifra de riesgo fundamental que se aplica para calcular los valores de coberturas.
# Retornos
ret_long <- retornos %>%
pivot_longer(-Date, names_to = "Ticker", values_to = "Return")
ggplot(ret_long, aes(Date, Return, color = Ticker)) +
geom_line(size = 0.8) +
theme_minimal() +
labs(title = "Retornos logaritmicos diarios", y = "Retorno diario")
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Análisis fundamental TXN pertenece al sector tecnológico de
semiconductores, con una expectativa positiva ante la creciente demanda
de chips. PEP está en el sector de consumo básico, con un crecimiento
estable y alta resistencia a la inflación. UPS, por su parte opera en el
sector logístico, afectado por la desaceleración económica global pero
con proyecciones de recuperación gradual. En conjunto, estas tres
acciones se espera una tendencia lateral-alcista a julio de 2025.
# Cálculo del Sharpe Ratio del portafolio óptimo
# Tasa libre de riesgo anual
rf <- 0.045
# Extraer los valores desde el objeto opt
ret_esperado <- opt$pm # retorno promedio diario
desv_port <- opt$ps # desviación estándar diaria
# Calcular el Sharpe Ratio anualizado
sharpe <- ((ret_esperado * 252) - rf) / (desv_port * sqrt(252))
sharpe
## [1] 1.5689
# Mostrar resumen general del portafolio
cat("Pesos óptimos del portafolio:\n")
## Pesos óptimos del portafolio:
print(opt$pw)
## [1] 0.16043577 0.77140070 0.06816353
cat("\nRetorno anual esperado:", round(ret_esperado * 252, 4))
##
## Retorno anual esperado: 1.0762
cat("\nDesviacion anual:", round(desv_port * sqrt(252), 4))
##
## Desviacion anual: 0.6573
cat("\nSharpe Ratio:", round(sharpe, 4))
##
## Sharpe Ratio: 1.5689
ggplot(retornos, aes(x = ret_TXN, y = ret_PEP)) +
geom_point(alpha = 0.3, color = "black") +
labs(title = "Correlacion entre TXN y PEP", x = "Retorno TXN", y = "Retorno PEP")
#visualización simple
Acc_long <- Acciones %>%
pivot_longer(-Date, names_to = "Ticker", values_to = "Price")
ggplot(Acc_long, aes(Date, Price, color = Ticker)) +
geom_line(size = 1) +
theme_minimal() +
labs(title = "Evolucion de precios - TXN, PEP, UPS", y = "Precio (USD)")
#Conclusiones finales El portafolio elaborado muestra una buena diversificación entre sectores, combinando empresas de tecnología, consumo y transporte. Los retornos esperados se ajustan a lo que normalmente se observa en cada uno de estos sectores, lo que indica una selección coherente de activos. Además, la volatilidad del conjunto es menor que la de los activos por separado, lo que demuestra que la diversificación está cumpliendo su función al reducir el riesgo total. La beta del portafolio refleja una sensibilidad media frente al mercado, representado por el índice S&P 500, por lo que no se considera ni demasiado agresivo ni demasiado conservador. Por su parte, el VaR obtenido señala que el nivel de riesgo asumido es razonable en relación con el capital invertido. En conclusión, el portafolio cumple con los criterios de eficiencia y control del riesgo planteados, por lo que se puede considerar que la primera parte del trabajo final se encuentra correctamente desarrollada.
#Numero optimo de contratos
num_contratos_optimos <- (beta_port * C_portafolio) / (S_indice * Q_futuro)
cat("\nNúmero Óptimo de Contratos a Vender (N*):", round(num_contratos_optimos, 2), "\n")
##
## Número Óptimo de Contratos a Vender (N*): 2.75
#Valor esperado de la cobertura trimestral
t_trimestral <- 3/12
F_esperado <- S_indice * exp(rf_anual * t_trimestral)
ganancia_esperada_futuro <- (F_esperado - S_indice) * Q_futuro
VEC_trimestral <- num_contratos_optimos * ganancia_esperada_futuro
cat("\nValor Esperado de la Cobertura Trimestral (VEC):", round(VEC_trimestral, 2), "\n")
##
## Valor Esperado de la Cobertura Trimestral (VEC): 42719.86