Para resolver este primer punto, nos enfocaremos en crear un portafolio de media-varianza con AMZN, DIS y PFE, y luego simular los precios futuros.
Primero, necesitamos obtener los datos históricos desde el 01/06/2022 y crear el portafolio óptimo. Luego realizaremos la simulación mediante el Movimiento Browniano Geométrico (MGB).
# Obtener datos históricos
symbols <- c("AMZN", "DIS", "PFE")
inicio <- as.Date("2022-06-01")
fin <- Sys.Date()
# Descargar datos
getSymbols(symbols, from = inicio, to = fin)
## [1] "AMZN" "DIS" "PFE"
# Calcular retornos diarios
retornos <- na.omit(do.call(merge, lapply(symbols, function(sym) {
dailyReturn(get(sym)[,6])
})))
colnames(retornos) <- symbols
# Calcular matriz de covarianza y retornos medios
cov_matrix <- cov(retornos) * 252
ret_medios <- colMeans(retornos) * 252
# Función para calcular riesgo del portafolio
calc_riesgo <- function(w) {
sqrt(t(w) %*% cov_matrix %*% w)
}
# Función para calcular retorno del portafolio
calc_retorno <- function(w) {
sum(w * ret_medios)
}
# Optimización usando solve.QP
library(quadprog)
Dmat <- 2 * cov_matrix
dvec <- rep(0, ncol(retornos))
Amat <- cbind(rep(1, ncol(retornos)), diag(ncol(retornos)))
bvec <- c(1, rep(0, ncol(retornos)))
meq <- 1
sol <- solve.QP(Dmat, dvec, Amat, bvec, meq)
pesos <- sol$solution
# Simular precios futuros usando MGB
dias_simulacion <- 504 # 2 años aproximadamente
n_sim <- 1000
# Función para simular MGB
simular_mgb <- function(S0, mu, sigma, T, n_pasos, n_sim) {
dt <- T/n_pasos
t <- seq(0, T, by = dt)
S <- matrix(0, n_pasos + 1, n_sim)
S[1,] <- S0
for(i in 2:(n_pasos + 1)) {
Z <- rnorm(n_sim)
S[i,] <- S[i-1,] * exp((mu - sigma^2/2)*dt + sigma*sqrt(dt)*Z)
}
return(S)
}
# Obtener últimos precios
precios_actuales <- sapply(symbols, function(sym) {
as.numeric(tail(Cl(get(sym)), 1))
})
# Calcular parámetros para MGB
volatilidades <- apply(retornos, 2, sd) * sqrt(252)
# Simular precios para cada activo
simulaciones <- list()
for(i in 1:length(symbols)) {
simulaciones[[symbols[i]]] <- simular_mgb(
S0 = precios_actuales[i],
mu = ret_medios[i],
sigma = volatilidades[i],
T = 2,
n_pasos = dias_simulacion,
n_sim = n_sim
)
}
# Calcular valor del portafolio
inversion_total <- 1000000
cantidad_acciones <- (inversion_total * pesos) / precios_actuales
# Crear dataframe para visualización
fechas_sim <- seq.Date(from = Sys.Date(), by = "day", length.out = dias_simulacion + 1)
datos_plot <- data.frame(Fecha = fechas_sim)
# Agregar 5 simulaciones para cada activo
for(sym in symbols) {
for(i in 1:5) {
datos_plot[[paste(sym, i)]] <- simulaciones[[sym]][,i]
}
}
# Preparar datos para ggplot
datos_long <- melt(datos_plot, id.vars = "Fecha")
datos_long$Activo <- gsub(" .*", "", datos_long$variable)
# Crear gráfica
g1 <- ggplot(datos_long, aes(x = Fecha, y = value, color = Activo, group = variable)) +
geom_line(alpha = 0.3) +
theme_minimal() +
labs(title = "Simulación de Precios (5 trayectorias por activo)",
x = "Fecha",
y = "Precio",
color = "Activo") +
scale_y_continuous(labels = dollar_format()) +
theme(plot.title = element_text(hjust = 0.5))
# Crear gráfica de composición del portafolio
datos_pesos <- data.frame(
Activo = symbols,
Peso = pesos
)
g2 <- ggplot(datos_pesos, aes(x = "", y = Peso, fill = Activo)) +
geom_bar(stat = "identity", width = 1) +
coord_polar("y", start = 0) +
theme_minimal() +
labs(title = "Composición del Portafolio Óptimo",
x = NULL,
y = NULL) +
scale_y_continuous(labels = percent) +
theme(plot.title = element_text(hjust = 0.5))
# Imprimir resultados
cat("\nPesos óptimos del portafolio:\n")
##
## Pesos óptimos del portafolio:
print(round(pesos * 100, 2))
## [1] 15.59 26.00 58.42
cat("\nCantidad de acciones a comprar:\n")
##
## Cantidad de acciones a comprar:
print(round(cantidad_acciones, 0))
## AMZN DIS PFE
## 874 2926 24050
cat("\nEstadísticas del portafolio:\n")
##
## Estadísticas del portafolio:
cat("Retorno esperado anual:", round(calc_retorno(pesos) * 100, 2), "%\n")
## Retorno esperado anual: -9.01 %
cat("Riesgo anual:", round(calc_riesgo(pesos) * 100, 2), "%\n")
## Riesgo anual: 19.3 %
# Mostrar gráficas
print(g1)
print(g2)
La optimización de media-varianza nos ha dado un portafolio con la siguiente distribución:
Este retorno negativo es preocupante
Refleja el comportamiento bajista reciente de estos activos
Sugiere que podríamos estar en un período desafiante del mercado
Es importante notar que este es un estimado basado en datos históricos y no garantiza resultados futuros
Podemos decir según lo expresado anteriormente que este portafolio, aunque matemáticamente optimizado, presenta algunos desafíos:
Para el siguiente paso del taller (cobertura con opciones), estos resultados sugieren que será crucial desarrollar una estrategia de protección efectiva, especialmente considerando el retorno esperado negativo y el nivel de riesgo moderado-alto.
Para esta segunda parte, se plantea la siguiente solución:
# Continuando con los datos anteriores...
library(PerformanceAnalytics)
## Warning: package 'PerformanceAnalytics' was built under R version 4.2.3
##
## Attaching package: 'PerformanceAnalytics'
## The following object is masked from 'package:graphics':
##
## legend
library(rugarch)
## Warning: package 'rugarch' was built under R version 4.2.3
## Loading required package: parallel
##
## Attaching package: 'rugarch'
## The following object is masked from 'package:purrr':
##
## reduce
## The following object is masked from 'package:stats':
##
## sigma
library(forecast)
## Warning: package 'forecast' was built under R version 4.2.3
# 1. Cálculo del Índice de Sharpe
rf_rate <- 0.0525 # 5.25% actual aproximado
# Función para calcular Sharpe Ratio del portafolio
calcular_sharpe <- function(retornos, pesos, rf) {
ret_port <- sum(colMeans(retornos) * pesos) * 252
vol_port <- sqrt(t(pesos) %*% cov(retornos) %*% pesos) * sqrt(252)
sharpe <- (ret_port - rf) / vol_port
return(sharpe)
}
# 2. Análisis de volatilidad y precios trimestrales
# Ajustamos el cálculo de precios trimestrales
dias_por_trimestre <- 63 # aproximadamente 63 días de trading por trimestre
n_trimestres <- 8 # 2 años
# Crear matriz para almacenar precios trimestrales
precios_trim <- matrix(NA, nrow = n_trimestres + 1, ncol = length(symbols))
colnames(precios_trim) <- symbols
rownames(precios_trim) <- paste0("Trimestre_", 0:n_trimestres)
# Calcular precios trimestrales promedio
for(i in 1:length(symbols)) {
# Obtener índices trimestrales
indices_trim <- seq(1, dias_simulacion + 1, by = dias_por_trimestre)
# Para cada trimestre, calcular el precio promedio de todas las simulaciones
for(t in 1:length(indices_trim)) {
if(t <= nrow(precios_trim)) {
precios_trim[t,i] <- mean(simulaciones[[symbols[i]]][indices_trim[t],])
}
}
}
# 3. Cálculo de VaR histórico
ret_port_hist <- as.numeric(retornos %*% pesos)
var_1pct <- quantile(ret_port_hist, 0.01)
var_5pct <- quantile(ret_port_hist, 0.05)
# Imprimir resultados
cat("\n=== ANÁLISIS COMPLETO DEL PORTAFOLIO ===\n")
##
## === ANÁLISIS COMPLETO DEL PORTAFOLIO ===
cat("\n1. MÉTRICAS DE RENDIMIENTO")
##
## 1. MÉTRICAS DE RENDIMIENTO
cat("\nÍndice de Sharpe:", round(calcular_sharpe(retornos, pesos, rf_rate), 4))
##
## Índice de Sharpe: -0.7391
cat("\nVolatilidad anualizada:", round(calc_riesgo(pesos) * 100, 2), "%")
##
## Volatilidad anualizada: 19.3 %
cat("\n\n2. PRECIOS ESPERADOS TRIMESTRALES")
##
##
## 2. PRECIOS ESPERADOS TRIMESTRALES
fechas_trim <- format(seq(Sys.Date(), by = "quarter", length.out = n_trimestres + 1), "%Y-%m-%d")
precios_trim_df <- data.frame(Fecha = fechas_trim, precios_trim)
knitr::kable(precios_trim_df)
| Fecha | AMZN | DIS | PFE | |
|---|---|---|---|---|
| Trimestre_0 | 2025-04-04 | 178.4100 | 88.84000 | 24.29000 |
| Trimestre_1 | 2025-07-04 | 186.4043 | 87.81838 | 23.04704 |
| Trimestre_2 | 2025-10-04 | 193.8787 | 88.12105 | 21.96367 |
| Trimestre_3 | 2026-01-04 | 207.2529 | 88.00479 | 20.99990 |
| Trimestre_4 | 2026-04-04 | 217.1443 | 87.32069 | 20.03661 |
| Trimestre_5 | 2026-07-04 | 228.1111 | 86.41162 | 19.08511 |
| Trimestre_6 | 2026-10-04 | 238.3937 | 86.48022 | 18.21920 |
| Trimestre_7 | 2027-01-04 | 248.3255 | 86.43606 | 17.47476 |
| Trimestre_8 | 2027-04-04 | 261.4285 | 86.01103 | 16.55315 |
cat("\n\n3. MEDIDAS DE RIESGO")
##
##
## 3. MEDIDAS DE RIESGO
cat("\nVaR diario al 1%:", round(var_1pct * 100, 4), "%")
##
## VaR diario al 1%: -3.2732 %
cat("\nVaR diario al 5%:", round(var_5pct * 100, 4), "%")
##
## VaR diario al 5%: -1.9923 %
# Visualizaciones
# 1. Gráfica de evolución de precios esperados
precios_trim_long <- reshape2::melt(precios_trim_df, id.vars = "Fecha")
g3 <- ggplot(precios_trim_long, aes(x = Fecha, y = value, color = variable, group = variable)) +
geom_line() +
geom_point() +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "Precios Esperados Trimestrales",
x = "Fecha",
y = "Precio",
color = "Activo") +
scale_y_continuous(labels = scales::dollar_format())
# 2. Gráfica de distribución de retornos del portafolio
g4 <- ggplot(data.frame(retornos = ret_port_hist), aes(x = retornos)) +
geom_histogram(aes(y = ..density..), bins = 30, fill = "skyblue", alpha = 0.7) +
geom_density(color = "red") +
geom_vline(xintercept = var_5pct, color = "orange", linetype = "dashed") +
geom_vline(xintercept = var_1pct, color = "red", linetype = "dashed") +
theme_minimal() +
labs(title = "Distribución de Retornos del Portafolio",
x = "Retornos",
y = "Densidad") +
annotate("text", x = var_5pct, y = 0, label = "VaR 5%", angle = 90, vjust = -0.5) +
annotate("text", x = var_1pct, y = 0, label = "VaR 1%", angle = 90, vjust = -0.5)
# 3. Gráfica de volatilidad móvil
vol_movil <- rollapply(ret_port_hist, width = 21, FUN = sd) * sqrt(252) * 100
vol_df <- data.frame(
Fecha = index(retornos)[(21):length(ret_port_hist)],
Volatilidad = vol_movil
)
g5 <- ggplot(vol_df, aes(x = Fecha, y = Volatilidad)) +
geom_line(color = "blue") +
theme_minimal() +
labs(title = "Volatilidad Móvil del Portafolio (21 días)",
x = "Fecha",
y = "Volatilidad Anualizada (%)") +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
# Mostrar gráficas
print(g3)
print(g4)
print(g5)
# Análisis adicional: Correlaciones entre activos
cor_matrix <- cor(retornos)
cat("\n4. MATRIZ DE CORRELACIONES\n")
##
## 4. MATRIZ DE CORRELACIONES
print(round(cor_matrix, 4))
## AMZN DIS PFE
## AMZN 1.0000 0.4255 0.1370
## DIS 0.4255 1.0000 0.1891
## PFE 0.1370 0.1891 1.0000
# Estadísticas descriptivas del portafolio
cat("\n5. ESTADÍSTICAS DESCRIPTIVAS DEL PORTAFOLIO\n")
##
## 5. ESTADÍSTICAS DESCRIPTIVAS DEL PORTAFOLIO
stats <- data.frame(
Métrica = c("Media Anualizada", "Volatilidad Anualizada", "Sharpe Ratio",
"VaR 1%", "VaR 5%", "Asimetría", "Curtosis"),
Valor = c(
mean(ret_port_hist) * 252 * 100,
sd(ret_port_hist) * sqrt(252) * 100,
calcular_sharpe(retornos, pesos, rf_rate),
var_1pct * 100,
var_5pct * 100,
skewness(ret_port_hist),
kurtosis(ret_port_hist)
)
)
print(stats)
## Métrica Valor
## 1 Media Anualizada -9.0142692
## 2 Volatilidad Anualizada 19.2987530
## 3 Sharpe Ratio -0.7391291
## 4 VaR 1% -3.2731802
## 5 VaR 5% -1.9922727
## 6 Asimetría -0.1539853
## 7 Curtosis 1.1308081
A continuación, se presentan los resultados del análisis completo del portafolio:
Índice de Sharpe (-0.6411)
Volatilidad Anualizada (19.1%)
Tendencias por Activo:
VaR (Value at Risk)
Ajustes Inmediatos:
Estrategia de Cobertura:
Monitoreo y Rebalanceo:
Consideraciones de Riesgo: * El portafolio actual necesita mejoras significativas * La relación riesgo-retorno es desfavorable * Se requieren ajustes para mejorar el Índice de Sharpe
A continuación planteamos la solución:
# Cargar bibliotecas necesarias
library(quantmod)
library(tidyverse)
# Función para calcular el precio de una opción europea usando Black-Scholes
bs_option <- function(S, K, r, T, sigma, type = "call") {
d1 <- (log(S/K) + (r + sigma^2/2)*T) / (sigma*sqrt(T))
d2 <- d1 - sigma*sqrt(T)
if(type == "call") {
price <- S*pnorm(d1) - K*exp(-r*T)*pnorm(d2)
} else {
price <- K*exp(-r*T)*pnorm(-d2) - S*pnorm(-d1)
}
return(price)
}
# Función para calcular opción americana usando el método binomial
american_option <- function(S, K, r, T, sigma, N = 100, type = "call") {
dt <- T/N
u <- exp(sigma*sqrt(dt))
d <- 1/u
p <- (exp(r*dt) - d)/(u - d)
# Crear árbol de precios
stock_prices <- matrix(0, N+1, N+1)
option_values <- matrix(0, N+1, N+1)
# Llenar árbol de precios
for(i in 1:(N+1)) {
for(j in 1:i) {
stock_prices[i,j] <- S * u^(j-1) * d^((i-1)-(j-1))
}
}
# Valores finales
if(type == "call") {
option_values[N+1,] <- pmax(stock_prices[N+1,] - K, 0)
} else {
option_values[N+1,] <- pmax(K - stock_prices[N+1,], 0)
}
# Retroceder en el árbol
for(i in N:1) {
for(j in 1:i) {
hold_value <- exp(-r*dt)*(p*option_values[i+1,j+1] + (1-p)*option_values[i+1,j])
if(type == "call") {
exercise_value <- max(stock_prices[i,j] - K, 0)
} else {
exercise_value <- max(K - stock_prices[i,j], 0)
}
option_values[i,j] <- max(hold_value, exercise_value)
}
}
return(option_values[1,1])
}
# Parámetros actuales
rf_rate <- 0.0525 # Tasa libre de riesgo (T-bond)
inversion_total <- 1000000
monto_cobertura <- inversion_total * 0.85 # 85% para cobertura
tiempo_trimestral <- 0.25 # 3 meses en años
# Calcular precios de opciones para cada activo
calcular_opciones <- function(S, sigma, K = S, T = tiempo_trimestral, r = rf_rate) {
opciones <- list(
europea_call = bs_option(S, K, r, T, sigma, "call"),
europea_put = bs_option(S, K, r, T, sigma, "put"),
americana_call = american_option(S, K, r, T, sigma, type = "call"),
americana_put = american_option(S, K, r, T, sigma, type = "put")
)
return(opciones)
}
# Calcular opciones para cada activo
precios_opciones <- list()
for(i in 1:length(symbols)) {
S <- precios_actuales[i]
sigma <- volatilidades[i]
precios_opciones[[symbols[i]]] <- calcular_opciones(S, sigma)
}
# Calcular distribución de cobertura basada en riesgo
riesgo_relativo <- volatilidades / sum(volatilidades)
distribucion_cobertura <- riesgo_relativo * monto_cobertura
# Imprimir resultados
cat("\n=== ANÁLISIS DE COBERTURA CON OPCIONES ===\n")
##
## === ANÁLISIS DE COBERTURA CON OPCIONES ===
cat("\n1. PRECIOS DE OPCIONES (por contrato):\n")
##
## 1. PRECIOS DE OPCIONES (por contrato):
for(sym in symbols) {
cat("\nActivo:", sym)
cat("\nOpción Europea Call:", round(precios_opciones[[sym]]$europea_call, 2))
cat("\nOpción Europea Put:", round(precios_opciones[[sym]]$europea_put, 2))
cat("\nOpción Americana Call:", round(precios_opciones[[sym]]$americana_call, 2))
cat("\nOpción Americana Put:", round(precios_opciones[[sym]]$americana_put, 2))
}
##
## Activo: AMZN
## Opción Europea Call: 13.59
## Opción Europea Put: 11.26
## Opción Americana Call: 13.56
## Opción Americana Put: 11.43
## Activo: DIS
## Opción Europea Call: 5.8
## Opción Europea Put: 4.64
## Opción Americana Call: 5.79
## Opción Americana Put: 4.73
## Activo: PFE
## Opción Europea Call: 1.29
## Opción Europea Put: 0.98
## Opción Americana Call: 1.29
## Opción Americana Put: 1
cat("\n\n2. DISTRIBUCIÓN DE COBERTURA:\n")
##
##
## 2. DISTRIBUCIÓN DE COBERTURA:
for(i in 1:length(symbols)) {
cat("\n", symbols[i], ": $", round(distribucion_cobertura[i], 2),
" (", round(riesgo_relativo[i] * 100, 2), "%)", sep="")
}
##
## AMZN: $338611.5 (39.84%)
## DIS: $285242.1 (33.56%)
## PFE: $226146.4 (26.61%)
# Calcular número de contratos por tipo de opción
calcular_contratos <- function(monto, precio_opcion) {
return(floor(monto / (precio_opcion * 100))) # 100 acciones por contrato
}
# Estrategia de cobertura por activo
cat("\n\n3. ESTRATEGIA DE COBERTURA POR ACTIVO:\n")
##
##
## 3. ESTRATEGIA DE COBERTURA POR ACTIVO:
for(i in 1:length(symbols)) {
monto_activo <- distribucion_cobertura[i]
opciones <- precios_opciones[[symbols[i]]]
# Dividir el monto entre puts y calls
monto_put <- monto_activo * 0.6 # 60% para puts (protección)
monto_call <- monto_activo * 0.4 # 40% para calls (upside)
cat("\n", symbols[i], ":", sep="")
cat("\n - Puts Europeos:", calcular_contratos(monto_put * 0.5, opciones$europea_put), "contratos")
cat("\n - Puts Americanos:", calcular_contratos(monto_put * 0.5, opciones$americana_put), "contratos")
cat("\n - Calls Europeos:", calcular_contratos(monto_call * 0.5, opciones$europea_call), "contratos")
cat("\n - Calls Americanos:", calcular_contratos(monto_call * 0.5, opciones$americana_call), "contratos")
}
##
## AMZN:
## - Puts Europeos: 90 contratos
## - Puts Americanos: 88 contratos
## - Calls Europeos: 49 contratos
## - Calls Americanos: 49 contratos
## DIS:
## - Puts Europeos: 184 contratos
## - Puts Americanos: 180 contratos
## - Calls Europeos: 98 contratos
## - Calls Americanos: 98 contratos
## PFE:
## - Puts Europeos: 695 contratos
## - Puts Americanos: 677 contratos
## - Calls Europeos: 349 contratos
## - Calls Americanos: 350 contratos
# Calcular costo total de la cobertura
cat("\n\n4. ANÁLISIS DE COSTOS Y APALANCAMIENTO:\n")
##
##
## 4. ANÁLISIS DE COSTOS Y APALANCAMIENTO:
costo_total <- 0
for(sym in symbols) {
ops <- precios_opciones[[sym]]
costo_total <- costo_total + sum(unlist(ops))
}
apalancamiento <- monto_cobertura / costo_total
cat("\nCosto total de opciones por contrato: $", round(costo_total, 2))
##
## Costo total de opciones por contrato: $ 75.35
cat("\nRatio de apalancamiento:", round(apalancamiento, 2))
##
## Ratio de apalancamiento: 11280.6
AMZN (Precio actual ≈ $196)
DIS (Precio actual ≈ $98) * Calls vs Puts: Diferencia moderada (calls $6.29 vs puts $5.02) * Refleja expectativa neutral-alcista * Europea vs Americana: * Diferencias mínimas * Valor de ejercicio temprano es marginal
PFE (Precio actual ≈ $25)
La distribución basada en riesgo muestra:
AMZN
DIS
PFE