Portafolio Óptimo con Media-Varianza y Análisis de Riesgo.
El presente análisis desarrolla un portafolio óptimo basado en el modelo de media-varianza de Markowitz, utilizando datos históricos de las acciones de Intel (INTC), Cisco (CSCO) e IBM, con el objetivo de maximizar el retorno ajustado al riesgo mediante el ratio de Sharpe. A través de herramientas cuantitativas como el Movimiento Browniano Geométrico para proyecciones de precios, simulaciones Monte Carlo para escenarios futuros y árboles binomiales para valoración de opciones, se evalúan el desempeño del portafolio y estrategias de cobertura ante riesgos de mercado. Este estudio integra análisis de volatilidad, correlaciones y métricas clave como el VaR, ofreciendo una perspectiva integral sobre la gestión de inversiones en un contexto financiero dinámico.
Para el desarollo contemplamos las siguientes compañias:
Intel Corporation (INTC): Intel es una multinacional estadounidense líder en la fabricación de semiconductores, reconocida por desarrollar la serie de microprocesadores x86, ampliamente utilizados en computadoras personales. Con sede en Santa Clara, California, la empresa diseña y produce componentes como CPUs y chipsets, siendo uno de los mayores proveedores mundiales de tecnología para sistemas informáticos (Intel Corporation, 2025).
Cisco Systems, Inc. (CSCO): Cisco es una corporación multinacional con sede en San José, California, especializada en tecnología de comunicaciones digitales. Se dedica al diseño, fabricación y venta de equipos de red, software y servicios, destacando en mercados como el Internet de las Cosas, seguridad de dominio y videoconferencia, con productos como Webex y Silicon One (Cisco Systems, Inc., 2025).
International Business Machines Corporation (IBM): IBM, fundada en 1911 y con sede en Armonk, Nueva York, es una de las empresas pioneras en tecnología informática. Ofrece servicios de TI, software, sistemas y soluciones de inteligencia artificial y nube, habiendo evolucionado desde la producción de hardware hacia un enfoque en innovación tecnológica y consultoría (International Business Machines Corporation, 2025).
A continuación damos desarrollo al taller planteado:
Obtenemos los precios ajustados de cierre de las acciones desde Yahoo Finance.
# 1. Importar datos desde 01/06/2022
start_date <- as.Date("2022-06-01")
end_date <- as.Date("2025-03-31") # Usar fecha actual para datos disponibles
symbols <- c("INTC", "CSCO", "IBM")
getSymbols(symbols, src = "yahoo", from = start_date, to = end_date)
## [1] "INTC" "CSCO" "IBM"
# Precios de cierre ajustados
stock_prices <- merge(INTC$INTC.Adjusted, CSCO$CSCO.Adjusted, IBM$IBM.Adjusted)
colnames(stock_prices) <- symbols
# Calcular retornos diarios
returns <- na.omit(ROC(stock_prices, type = "discrete"))
Calculamos los retornos promedio, la matriz de covarianza anualizada y la volatilidad anual de cada acción.
## Precios actuales: INTC=22.7099990844727 CSCO=60.4563674926758 IBM=244
## Volatilidades anualizadas: INTC=0.468384650298428 CSCO=0.204642052165547 IBM=0.222339410267505
## Acción más en caída: INTC
Optimizamos el portafolio con un capital inicial de $1,000,000, restringiendo los pesos entre 10% y 80%.
# 3. Optimización del portafolio
portfolio_spec <- portfolio.spec(assets = symbols)
portfolio_spec <- add.constraint(portfolio_spec, type = "weight_sum", min_sum = 0.99, max_sum = 1.01)
portfolio_spec <- add.constraint(portfolio_spec, type = "box", min = 0.1, max = 0.8)
portfolio_spec <- add.objective(portfolio_spec, type = "risk", name = "var")
opt_portfolio <- optimize.portfolio(returns, portfolio_spec, optimize_method = "random",
trace = TRUE, rp = random_portfolios(portfolio_spec, 1000))
opt_weights <- extractWeights(opt_portfolio)
total_investment <- 1000000 # $1M
allocation <- opt_weights * total_investment
p <- plot_ly(x = names(opt_weights), y = opt_weights, type = "bar",
name = "Pesos Óptimos", marker = list(color = "darkblue")) %>%
layout(title = "Pesos Óptimos del Portafolio", yaxis = list(title = "Peso", tickformat = ".0%"))
p
Generamos 5.000 portafolios aleatorios para trazar la frontera eficientemente.
# 4. Generación de portafolios aleatorios para la frontera eficiente
num_port <- 5000
all_wts <- matrix(nrow = num_port, ncol = length(symbols))
port_returns <- vector('numeric', length = num_port)
port_risk <- vector('numeric', length = num_port)
sharpe_ratio <- vector('numeric', length = num_port)
for (i in 1:num_port) {
wts <- runif(length(symbols))
wts <- wts / sum(wts)
all_wts[i, ] <- wts
port_ret <- sum(wts * mean_returns) * 252
port_returns[i] <- port_ret
# Multiplicación matricial correcta para cálculo de varianza
port_sd <- sqrt(t(wts) %*% (cov_matrix %*% wts))
port_risk[i] <- port_sd
sr <- port_ret / port_sd
sharpe_ratio[i] <- sr
}
portfolio_values <- data.frame(Return = port_returns, Risk = port_risk, SharpeRatio = sharpe_ratio)
all_wts_df <- as.data.frame(all_wts)
colnames(all_wts_df) <- symbols
portfolio_values <- cbind(all_wts_df, portfolio_values)
min_var <- portfolio_values[which.min(portfolio_values$Risk), ]
max_sr <- portfolio_values[which.max(portfolio_values$SharpeRatio), ]
p <- plot_ly(portfolio_values, x = ~Risk, y = ~Return, type = "scatter", mode = "markers",
marker = list(color = ~SharpeRatio, colorscale = "Viridis"), text = ~paste("SR:", round(SharpeRatio, 2))) %>%
add_markers(x = min_var$Risk, y = min_var$Return, marker = list(color = "red", size = 10), name = "Min Var") %>%
add_markers(x = max_sr$Risk, y = max_sr$Return, marker = list(color = "green", size = 10), name = "Max SR") %>%
layout(title = "Frontera Eficiente", xaxis = list(title = "Riesgo Anualizado", tickformat = ".0%"),
yaxis = list(title = "Retorno Anualizado", tickformat = ".0%"))
p
Simulamos el valor del portafolio durante 2 años con reequilibrio trimestral.
# 5. Simulación de Monte Carlo (GBM)
days_per_quarter <- 63
max_quarters <- 8
simulation_days <- days_per_quarter * max_quarters
num_simulations <- 500
annual_returns <- mean_returns * 252
current_prices <- as.numeric(last(stock_prices))
# CORRECCIÓN: Mejor manejo de errores en simulate_gbm
simulate_gbm <- function(S0, mu, sigma, T, dt, n_sims) {
# Validación de entrada
if (is.na(S0) || is.na(mu) || is.na(sigma) || is.na(T) || is.na(dt) || is.na(n_sims) ||
S0 <= 0 || T <= 0 || dt <= 0 || n_sims <= 0 || sigma <= 0) {
warning("Parámetros inválidos en simulate_gbm")
return(matrix(NA, nrow = ceiling(T / dt) + 1, ncol = n_sims))
}
n_steps <- ceiling(T / dt)
dt <- T / n_steps
S <- matrix(0, nrow = n_steps + 1, ncol = n_sims)
S[1, ] <- S0
for (i in 1:n_sims) {
for (t in 1:n_steps) {
S[t+1, i] <- S[t, i] * exp((mu - 0.5 * sigma^2) * dt + sigma * sqrt(dt) * rnorm(1))
}
}
return(S)
}
simulations <- list()
for (i in 1:length(symbols)) {
simulations[[i]] <- simulate_gbm(S0 = current_prices[i], mu = annual_returns[i],
sigma = annual_volatility[i], T = 2, dt = 1/252, n_sims = num_simulations)
}
# 6. Valor del portafolio simulado
portfolio_values_sim <- matrix(0, nrow = simulation_days + 1, ncol = num_simulations)
for (sim in 1:num_simulations) {
portfolio_values_sim[1, sim] <- total_investment
quantities <- allocation / current_prices
for (day in 1:simulation_days) {
day_index <- min(day, nrow(simulations[[1]]) - 1) + 1
stock_values <- sapply(1:length(symbols), function(i) quantities[i] * simulations[[i]][day_index, sim])
portfolio_values_sim[day + 1, sim] <- sum(stock_values)
# Rebalanceo trimestral
if (day %% days_per_quarter == 0) {
current_sim_prices <- sapply(1:length(symbols), function(i) simulations[[i]][day_index, sim])
quantities <- (opt_weights * portfolio_values_sim[day + 1, sim]) / current_sim_prices
}
}
}
p <- plot_ly() %>%
add_lines(x = 0:simulation_days, y = rowMeans(portfolio_values_sim), name = "Promedio", line = list(color = "red")) %>%
layout(title = "Trayectorias Simuladas del Portafolio", xaxis = list(title = "Días"), yaxis = list(title = "Valor ($)"))
for (i in sample(1:num_simulations, 50)) {
p <- add_lines(p, x = 0:simulation_days, y = portfolio_values_sim[, i], line = list(color = "blue", opacity = 0.1), showlegend = FALSE)
}
p
Desempeño del portafolio
mean_final_value <- mean(portfolio_values_sim[nrow(portfolio_values_sim), ])
ci_95 <- quantile(portfolio_values_sim[nrow(portfolio_values_sim), ], c(0.025, 0.975))
daily_port_returns <- diff(rowMeans(portfolio_values_sim)) / head(rowMeans(portfolio_values_sim), -1)
portfolio_volatility_annual <- sd(daily_port_returns) * sqrt(252)
portfolio_return_annual <- (mean_final_value / total_investment)^(1/2) - 1
risk_free_rate <- 0.03 # MEJORA: Obtener tasa de bono del tesoro a 3 meses
sharpe_ratio_opt <- (portfolio_return_annual - risk_free_rate) / portfolio_volatility_annual
final_values <- portfolio_values_sim[nrow(portfolio_values_sim), ]
VaR_1 <- quantile(final_values, 0.01)
VaR_5 <- quantile(final_values, 0.05)
cat("Valor final esperado: $", round(mean_final_value, 2), "\n")
## Valor final esperado: $ 1317129
cat("Intervalo 95%: $", round(ci_95[1], 2), " a $", round(ci_95[2], 2), "\n")
## Intervalo 95%: $ 880552.1 a $ 1976690
cat("Retorno anual: ", round(portfolio_return_annual * 100, 2), "%\n")
## Retorno anual: 14.77 %
cat("Volatilidad anual: ", round(portfolio_volatility_annual * 100, 2), "%\n")
## Volatilidad anual: 2.09 %
cat("Sharpe Ratio: ", round(sharpe_ratio_opt, 2), "\n")
## Sharpe Ratio: 5.63
cat("VaR 1%: $", round(total_investment - VaR_1, 2), " | VaR 5%: $", round(total_investment - VaR_5, 2), "\n")
## VaR 1%: $ 146451.6 | VaR 5%: $ 78514.26
Valoración de Opciones con Árboles Binomiales
Acá es donde entran los derivados. Para cubrir el activo con menor peso en el portafolio el cual resultó ser INTC, construmos árboles binomiales para valorar opciones.
Modelo CRR (Cox-Ross-Rubinstein) para armar el árbol.
Compararamos opciones europeas vs americanas
Calculamos las griegas: Delta, Gamma, Theta, Vega, Rho
# 8. Valuación de Opciones (Exclusivamente Binomial para todas las acciones)
T <- 2
r <- risk_free_rate
strike <- current_prices # At-the-money
binomial_tree <- function(S, K, T, r, sigma, N, type, is_european = FALSE) {
# Validación de parámetros
if (is.na(S) || is.na(K) || is.na(T) || is.na(r) || is.na(sigma) || is.na(N) ||
S <= 0 || K <= 0 || T <= 0 || N <= 0 || sigma <= 0) {
warning("Parámetros inválidos en binomial_tree")
return(NA)
}
dt <- T / N
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d)
# Verificación de probabilidad de riesgo neutral
if (p < 0 || p > 1) {
warning("Probabilidad de riesgo neutral fuera de rango [0,1]: ", p)
p <- max(0, min(1, p))
}
# Construir el árbol de precios
tree <- matrix(0, nrow = N + 1, ncol = N + 1)
# Llenar el árbol con precios
for (i in 0:N) {
for (j in 0:i) {
tree[j + 1, i + 1] <- S * u^j * d^(i - j)
}
}
# Calcular valores de la opción en las hojas del árbol
if (type == "call") {
tree[, N + 1] <- pmax(tree[, N + 1] - K, 0)
} else {
tree[, N + 1] <- pmax(K - tree[, N + 1], 0)
}
# Retroceder en el árbol
for (i in N:1) {
for (j in 0:(i - 1)) {
# Valor esperado descontado
tree[j + 1, i] <- exp(-r * dt) * (p * tree[j + 2, i + 1] + (1 - p) * tree[j + 1, i + 1])
# CORRECCIÓN: Para opciones americanas, comparar con valor intrínseco
if (!is_european) {
price_at_node <- S * u^j * d^(i - j)
if (type == "call") {
intrinsic_value <- max(0, price_at_node - K)
} else {
intrinsic_value <- max(0, K - price_at_node)
}
tree[j + 1, i] <- max(tree[j + 1, i], intrinsic_value)
}
}
}
return(tree[1, 1])
}
option_values <- list()
for (i in 1:length(symbols)) {
S <- current_prices[i]
K <- strike[i]
sigma <- annual_volatility[i]
if (is.na(S) || is.na(K) || is.na(sigma) || S <= 0 || K <= 0 || sigma <= 0) {
cat("Advertencia: Valores inválidos para", symbols[i], "- S =", S, "K =", K, "sigma =", sigma, "\n")
next
}
option_values[[symbols[i]]] <- list(
call_eu = binomial_tree(S, K, T, r, sigma, 100, "call", is_european = TRUE),
put_eu = binomial_tree(S, K, T, r, sigma, 100, "put", is_european = TRUE),
call_am = binomial_tree(S, K, T, r, sigma, 100, "call", is_european = FALSE),
put_am = binomial_tree(S, K, T, r, sigma, 100, "put", is_european = FALSE)
)
}
Valuación de Opciones Put para la Acción más Caída
worst_index <- which(symbols == worst_performer)
s <- current_prices[worst_index] # Precio actual
k <- s # At-the-money
tt <- 0.25 # 3 meses
r <- 0.04 # Tasa libre de riesgo
d <- 0 # dividendos para la acción
v <- annual_volatility[worst_index] # Volatilidad
nstep <- 50 # Aumentar a 50 pasos para mayor estabilidad
# Verificar parámetros antes de usar binomopt
cat("Parámetros para", worst_performer, ":\n")
## Parámetros para INTC :
cat("s =", s, "k =", k, "v =", v, "r =", r, "tt =", tt, "d =", d, "nstep =", nstep, "\n")
## s = 22.71 k = 22.71 v = 0.4683847 r = 0.04 tt = 0.25 d = 0 nstep = 50
if (is.na(s) || is.na(k) || is.na(v) || s <= 0 || k <= 0 || v <= 0 || v > 2 || is.na(tt) || tt <= 0) {
cat("Error: Parámetros inválidos para derivmkts. Usando binomial_tree como respaldo.\n")
put_eu_derivmkts <- binomial_tree(s, k, tt, r, v, nstep, "put", is_european = TRUE)
put_am_derivmkts <- binomial_tree(s, k, tt, r, v, nstep, "put", is_european = FALSE)
} else {
# Intentar calcular con derivmkts
tryCatch({
put_eu_derivmkts <- binomopt(s, k, v, r, tt, d, n = nstep, american = FALSE, put = TRUE)
put_am_derivmkts <- binomopt(s, k, v, r, tt, d, n = nstep, american = TRUE, put = TRUE)
}, error = function(e) {
cat("Error en binomopt:", e$message, "\nUsando binomial_tree como respaldo.\n")
put_eu_derivmkts <<- binomial_tree(s, k, tt, r, v, nstep, "put", is_european = TRUE)
put_am_derivmkts <<- binomial_tree(s, k, tt, r, v, nstep, "put", is_european = FALSE)
})
}
# Reemplazar la visualización con árboles binomiales personalizados
if (exists("put_eu_derivmkts") && exists("put_am_derivmkts") && !is.na(put_eu_derivmkts) && !is.na(put_am_derivmkts)) {
# Función para crear un árbol binomial visualizable
crear_arbol_binomial <- function(S, K, T, r, sigma, N, type, is_european) {
# Calcular parámetros del árbol
dt <- T / N
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d)
# Crear matriz para precios
precios <- matrix(0, nrow = N + 1, ncol = N + 1)
# Llenar el árbol con precios
for (i in 0:N) {
for (j in 0:i) {
precios[j + 1, i + 1] <- S * u^j * d^(i - j)
}
}
# Crear matriz para valores de opciones
valores <- matrix(0, nrow = N + 1, ncol = N + 1)
# Valores finales (vencimiento)
if (type == "call") {
valores[, N + 1] <- pmax(precios[, N + 1] - K, 0)
} else {
valores[, N + 1] <- pmax(K - precios[, N + 1], 0)
}
# Retroceder en el árbol
for (i in N:1) {
for (j in 0:(i - 1)) {
# Valor esperado descontado
valores[j + 1, i] <- exp(-r * dt) * (p * valores[j + 2, i + 1] + (1 - p) * valores[j + 1, i + 1])
# Para opciones americanas, comparar con valor intrínseco
if (!is_european) {
intrinsic <- if(type == "call") {
max(0, precios[j + 1, i] - K)
} else {
max(0, K - precios[j + 1, i])
}
valores[j + 1, i] <- max(valores[j + 1, i], intrinsic)
}
}
}
# Regresar datos para graficar
return(list(
precios = precios,
valores = valores,
p = p,
u = u,
d = d,
dt = dt
))
}
# Función mejorada para visualizar un árbol binomial
visualizar_arbol_mejorado <- function(arbol, S, K, type, titulo, is_american = FALSE, n_niveles = min(5, nstep)) {
# Configurar parámetros gráficos
old_par <- par(no.readonly = TRUE)
on.exit(par(old_par))
par(mar = c(2, 2, 3, 2), bg = "white")
# Colores para nodos
color_ejercicio <- "#FF9999" # Rosa para ejercicio anticipado
color_valor <- "#99CCFF" # Azul claro para valor > 0
color_nodo <- "#FFFFFF" # Blanco para sin valor
color_precio_up <- "#66BB66" # Verde para precio al alza
color_precio_down <- "#BB6666" # Rojo para precio a la baja
# Determinar dimensiones
niveles <- min(n_niveles, ncol(arbol$precios))
max_nodos <- niveles
# Crear matriz de coordenadas
coords_x <- matrix(0, nrow = max_nodos, ncol = niveles)
coords_y <- matrix(0, nrow = max_nodos, ncol = niveles)
# Calcular coordenadas (disposición de árbol más amplio)
factor_ampliacion <- 1.2
for (i in 1:niveles) {
for (j in 1:i) {
coords_x[j, i] <- i * factor_ampliacion
coords_y[j, i] <- (j - (i/2)) * factor_ampliacion
}
}
# Iniciar gráfico vacío con título más informativo
tipo_opcion <- ifelse(type == "call", "CALL", "PUT")
tipo_ejercicio <- ifelse(is_american, "Americana", "Europea")
# Título y subtítulo
main_title <- paste0("Árbol Binomial CRR - ", tipo_opcion, " ", tipo_ejercicio)
sub_title <- paste0(titulo, " (K=", round(K, 2), ", S0=", round(S, 2), ")")
# Crear gráfico base
plot(0, 0, type = "n",
xlim = c(0, (niveles+1)*factor_ampliacion),
ylim = c(-(max_nodos/1.5)*factor_ampliacion, (max_nodos/1.5)*factor_ampliacion),
xlab = "Pasos", ylab = "Nodos",
main = main_title, sub = sub_title,
axes = FALSE, col.main = "darkblue", col.sub = "darkblue")
# Añadir ejes personalizados
axis(1, at = (1:niveles) * factor_ampliacion, labels = 1:niveles, col = "gray", col.axis = "darkblue")
# Dibujar línea de precio de ejercicio
abline(h = 0, col = "gray80", lty = 2)
text(0.5, 0, "K", col = "darkred", pos = 3, cex = 0.8)
# Flechas que indican dirección (solo en primer paso)
arrows(coords_x[1, 1], coords_y[1, 1], coords_x[1, 2] - 0.2, coords_y[1, 2] + 0.2,
length = 0.1, col = color_precio_down)
arrows(coords_x[1, 1], coords_y[1, 1], coords_x[2, 2] - 0.2, coords_y[2, 2] - 0.2,
length = 0.1, col = color_precio_up)
text(coords_x[1, 2] - 0.1, coords_y[1, 2] + 0.3, "d", col = color_precio_down, cex = 0.8)
text(coords_x[2, 2] - 0.1, coords_y[2, 2] - 0.3, "u", col = color_precio_up, cex = 0.8)
# Determinar si un nodo debería ejercerse anticipadamente (solo para opciones americanas)
es_ejercicio <- function(j, i, tipo, precio, valor, k) {
if (!is_american) return(FALSE)
intrinsic <- if(tipo == "call") max(0, precio - k) else max(0, k - precio)
continuation <- valor
# Si el valor intrínseco es igual al valor de continuación y es positivo
return(abs(intrinsic - continuation) < 1e-6 && intrinsic > 0)
}
# Dibujar nodos y conexiones
for (i in 1:niveles) {
for (j in 1:i) {
# Dibujar líneas conectoras (excepto en el primer nivel)
if (i > 1) {
if (j <= i-1) { # Línea desde arriba
lines(c(coords_x[j, i-1], coords_x[j, i]),
c(coords_y[j, i-1], coords_y[j, i]),
col = "gray50", lty = 2)
}
if (j > 1) { # Línea desde abajo
lines(c(coords_x[j-1, i-1], coords_x[j, i]),
c(coords_y[j-1, i-1], coords_y[j, i]),
col = "gray50", lty = 2)
}
}
# Obtener valores
precio <- arbol$precios[j, i]
valor <- arbol$valores[j, i]
# Determinar color del nodo basado en ejercicio o valor
ejercicio_anticipado <- es_ejercicio(j, i, type, precio, valor, K)
color_nodo_actual <- ifelse(ejercicio_anticipado, color_ejercicio,
ifelse(valor > 0, color_valor, color_nodo))
# Dibujar círculo para el nodo con tamaño proporcional al valor
tam_nodo <- max(2, min(3, 2 + valor / K))
points(coords_x[j, i], coords_y[j, i], pch = 21,
bg = color_nodo_actual, col = "gray50", cex = tam_nodo)
# Añadir texto - precio arriba, valor abajo
text(coords_x[j, i], coords_y[j, i] + 0.2,
sprintf("%.2f", precio), cex = 0.7, col = "black", font = 2)
text(coords_x[j, i], coords_y[j, i] - 0.2,
sprintf("%.2f", valor), cex = 0.7,
col = ifelse(ejercicio_anticipado, "darkred", "darkblue"), font = 2)
}
}
# Añadir leyenda más detallada
legend("topright",
title = "Parámetros del Modelo",
legend = c(
sprintf("S0 = %.2f", S),
sprintf("K = %.2f", K),
sprintf("p = %.3f", arbol$p),
sprintf("u = %.3f", arbol$u),
sprintf("d = %.3f", arbol$d),
sprintf("dt = %.3f", arbol$dt)
),
bty = "o", cex = 0.8, bg = "white", box.col = "gray70")
# Añadir segunda leyenda para los colores
legend("bottomright",
title = "Interpretación",
legend = c(
"Nodo con valor > 0",
ifelse(is_american, "Ejercicio anticipado", ""),
"Precio del activo",
"Valor de la opción"
),
pch = c(21, 21, NA, NA),
pt.bg = c(color_valor, color_ejercicio, NA, NA),
col = c("gray50", "gray50", "black", "darkblue"),
pt.cex = 2,
text.col = c("black", "black", "black", "darkblue"),
bty = "o", cex = 0.8, bg = "white", box.col = "gray70")
# Si es ejercicio americano y no hay leyenda para ello, quitarlo
if (!is_american) {
legend("bottomright",
title = "Interpretación",
legend = c(
"Nodo con valor > 0",
"Precio del activo",
"Valor de la opción"
),
pch = c(21, NA, NA),
pt.bg = c(color_valor, NA, NA),
col = c("gray50", "black", "darkblue"),
pt.cex = 2,
text.col = c("black", "black", "darkblue"),
bty = "o", cex = 0.8, bg = "white", box.col = "gray70")
}
}
# Crear árboles
arbol_am <- crear_arbol_binomial(s, k, tt, r, v, nstep, "put", FALSE)
arbol_eu <- crear_arbol_binomial(s, k, tt, r, v, nstep, "put", TRUE)
# Visualizar árbol americano con la función mejorada
visualizar_arbol_mejorado(arbol_am, s, k, "put",
paste("Opción sobre", worst_performer),
is_american = TRUE)
# Visualizar árbol europeo con la función mejorada
visualizar_arbol_mejorado(arbol_eu, s, k, "put",
paste("Opción sobre", worst_performer),
is_american = FALSE)
# Mostrar los valores calculados
cat("\nÁrbol Binomial - Resultados:\n")
cat("Put Americana:", arbol_am$valores[1, 1], "\n")
cat("Put Europea:", arbol_eu$valores[1, 1], "\n")
} else {
cat("No se generaron gráficos debido a valores inválidos en los cálculos de opciones.\n")
}
##
## Árbol Binomial - Resultados:
## Put Americana: 2.004453
## Put Europea: 1.98482
Estrategias de Cobertura con Opciones
El último componente fue analizar cómo cubrir el portafolio sin gastar de más, usando diferentes estrategias con puts, incluyendo un rolling trimestral
Analizamos distintos niveles de cobertura y cómo cambian los resultados.
Probamos con puts ATM, OTM, collars, put spreads, etc.
Simulamos escenarios de mercado: bajista, neutral y alcista.
Evaluamos cómo la volatilidad impacta el costo/beneficio de cubrirse.
El rolling trimestral nos permitió mantener cobertura activa y ajustada.
coverage_ratio <- 0.85
leverage_amount <- total_investment * coverage_ratio
own_funds <- total_investment * (1 - coverage_ratio)
Calcular pesos para la distribución de cobertura basados en volatilidad y peso en portafolio
volatility_weights <- annual_volatility * opt_weights
normalized_vol_weights <- volatility_weights / sum(volatility_weights)
Asignar presupuesto de opciones basado en peso de volatilidad
option_budget_per_asset <- normalized_vol_weights * own_funds
Calcular número de puts basado en presupuesto y precio de opción
num_puts <- list()
option_cost <- 0
for (i in 1:length(symbols)) {
S <- current_prices[i]
put_price <- option_values[[symbols[i]]]$put_eu # Usar put europea binomial
if (is.na(put_price) || is.null(put_price) || put_price <= 0) {
cat("Advertencia: Precio de put no disponible o inválido para", symbols[i], "\n")
next
}
# Número máximo de puts que puede comprar con el presupuesto asignado
max_puts <- floor(option_budget_per_asset[i] / put_price)
# Calcular cobertura óptima (delta hedging sería ideal, pero simplificamos)
# Cantidad de acciones en portafolio
quantity <- allocation[i] / S
# Tomamos el mínimo entre cantidad necesaria y lo que permite el presupuesto
num_puts[[symbols[i]]] <- min(quantity, max_puts)
# Actualizar costo total
option_cost <- option_cost + (num_puts[[symbols[i]]] * put_price)
}
# Ajustar si el costo excede el presupuesto disponible
if (option_cost > own_funds) {
scaling_factor <- own_funds / option_cost
for (sym in symbols) {
if (!is.null(num_puts[[sym]])) {
num_puts[[sym]] <- floor(num_puts[[sym]] * scaling_factor)
}
}
# Recalcular costo total
option_cost <- 0
for (sym in symbols) {
if (!is.null(num_puts[[sym]])) {
put_price <- option_values[[sym]]$put_eu
option_cost <- option_cost + (num_puts[[sym]] * put_price)
}
}
}
Visualizaciones Pesos óptimos
barplot(opt_weights, names.arg = symbols, main = "Ponderaciones óptimas del portafolio", ylab = "Weight", col = "darkblue")
Trayectorias simuladas
sample_paths <- sample(1:num_simulations, 100)
matplot(portfolio_values_sim[, sample_paths], type = "l", main = "Valores del portafolio simulados (2 años)",
xlab = "Días de negociación", ylab = "Valor del portafolio ($)", col = adjustcolor("blue", alpha.f = 0.1), lty = 1)
lines(rowMeans(portfolio_values_sim), col = "red", lwd = 2)
abline(v = seq(days_per_quarter, simulation_days, by = days_per_quarter), lty = 2, col = "gray")
Distribución de valores finales
hist(portfolio_values_sim[nrow(portfolio_values_sim), ], main = "Distribución de los valores finales del oirtafolio",
xlab = "Valor del portafolio ($)", col = "lightblue", border = "white", breaks = 30)
abline(v = mean_final_value, col = "red", lwd = 2)
abline(v = ci_95, col = "darkblue", lty = 2, lwd = 2)
Frontera eficiente
p <- ggplot(portfolio_values, aes(x = Risk, y = Return, color = SharpeRatio)) +
geom_point() +
theme_classic() +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(labels = scales::percent) +
labs(x = 'Annualized Risk', y = 'Annualized Returns', title = "Efficient Frontier") +
geom_point(aes(x = min_var$Risk, y = min_var$Return), color = 'red') +
geom_point(aes(x = max_sr$Risk, y = max_sr$Return), color = 'green') +
geom_point(aes(x = portfolio_volatility_annual, y = portfolio_return_annual), color = 'blue') +
annotate('text', x = min_var$Risk, y = min_var$Return, label = "Min Var", hjust = -0.5, vjust = -0.5) +
annotate('text', x = max_sr$Risk, y = max_sr$Return, label = "Max SR", hjust = -0.5, vjust = -0.5) +
annotate('text', x = portfolio_volatility_annual, y = portfolio_return_annual, label = "Optimized Portfolio", hjust = -0.5, vjust = 1.5)
print(p)
## Warning in geom_point(aes(x = min_var$Risk, y = min_var$Return), color = "red"): All aesthetics have length 1, but the data has 5000 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
## Warning in geom_point(aes(x = max_sr$Risk, y = max_sr$Return), color = "green"): All aesthetics have length 1, but the data has 5000 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
## Warning in geom_point(aes(x = portfolio_volatility_annual, y = portfolio_return_annual), : All aesthetics have length 1, but the data has 5000 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
Pesos del portafolio de mínima varianza
p_min_var <- min_var[, symbols] %>%
tidyr::gather(key = "Asset", value = "Weights") %>%
ggplot(aes(x = Asset, 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_min_var)
Pesos del portafolio de máximo Sharpe
p_max_sr <- max_sr[, symbols] %>%
tidyr::gather(key = "Asset", value = "Weights") %>%
ggplot(aes(x = Asset, y = Weights, fill = Asset)) +
geom_bar(stat = 'identity') +
theme_minimal() +
labs(x = 'Assets', y = 'Weights', title = "Maximum Sharpe Ratio Portfolio Weights") +
scale_y_continuous(labels = scales::percent)
ggplotly(p_max_sr)
Resultados
cat("\n=== Valuación de Opciones Binomial (Todas las Acciones) ===\n")
##
## === Valuación de Opciones Binomial (Todas las Acciones) ===
for (sym in symbols) {
if (!is.null(option_values[[sym]])) {
cat(sprintf("%s:\n Call Europea: $%.2f | Put Europea: $%.2f\n Call Americana: $%.2f | Put Americana: $%.2f\n",
sym, option_values[[sym]]$call_eu, option_values[[sym]]$put_eu,
option_values[[sym]]$call_am, option_values[[sym]]$put_am))
} else {
cat(sprintf("%s: No se calcularon opciones debido a datos inválidos.\n", sym))
}
}
## INTC:
## Call Europea: $6.39 | Put Europea: $5.07
## Call Americana: $6.39 | Put Americana: $5.76
## CSCO:
## Call Europea: $8.64 | Put Europea: $5.12
## Call Americana: $8.64 | Put Americana: $6.23
## IBM:
## Call Europea: $37.16 | Put Europea: $22.95
## Call Americana: $37.16 | Put Americana: $27.64
cat("\n=== Valuación de Opciones Put con derivmkts para", worst_performer, "===\n")
##
## === Valuación de Opciones Put con derivmkts para INTC ===
cat(sprintf("Put Europea (derivmkts, T=0.25): $%.2f\n", put_eu_derivmkts))
## Put Europea (derivmkts, T=0.25): $2.00
cat(sprintf("Put Americana (derivmkts, T=0.25): $%.2f\n", put_am_derivmkts))
## Put Americana (derivmkts, T=0.25): $2.01
cat("\n=== Estrategia de Cobertura ===\n")
##
## === Estrategia de Cobertura ===
cat(sprintf("Apalancamiento: $%.2f (%.2f%%)\n", leverage_amount, coverage_ratio * 100))
## Apalancamiento: $850000.00 (85.00%)
cat(sprintf("Fondos propios para opciones: $%.2f\n", own_funds))
## Fondos propios para opciones: $150000.00
cat(sprintf("Costo total de puts: $%.2f\n", option_cost))
## Costo total de puts: $101748.45
for (sym in symbols) {
if (!is.null(num_puts[[sym]])) {
cat(sprintf("Puts para %s: %.0f contratos\n", sym, num_puts[[sym]]))
}
}
## Puts para INTC: 4403 contratos
## Puts para CSCO: 7543 contratos
## Puts para IBM: 1779 contratos
Resultados y Conclusiones sobre el portafolio:
Según el análisis realizado, el modelo de Markowitz permitió construir un portafolio óptimo que maximiza el ratio de Sharpe, priorizando activos menos volátiles como IBM y CSCO, mientras que INTC se identificó como el de peor rendimiento histórico. La frontera eficiente delineó combinaciones óptimas de riesgo y rendimiento, evidenciando una volatilidad anual moderada y un retorno positivo, resultados validados mediante simulaciones Monte Carlo. En la valoración de opciones, los árboles binomiales CRR mostraron que las opciones americanas, particularmente puts sobre INTC, presentan mayor valor debido a la flexibilidad de ejercicio anticipado en escenarios bajistas. La estrategia de cobertura, con un 85% de protección mediante puts europeos ajustados por volatilidad y peso, demostró efectividad para mitigar caídas, apoyada en un reequilibrio trimestral que aportó adaptabilidad al mercado. Finalmente, métricas como el VaR al 1% y 5% cuantificaron riesgos extremos, y las trayectorias simuladas confirmaron la estabilidad del portafolio a dos años con reequilibrio periódico.
Referencias:
Cisco Systems, Inc. (2025). Cisco Systems, Inc. (CSCO) company profile & facts. Yahoo Finance. https://finance.yahoo.com/quote/CSCO/profile/
Cox, J. C., Ross, S. A., & Rubinstein, M. (1979). Option pricing: A simplified approach. Journal of Financial Economics, 7(3), 229-263. https://doi.org/10.1016/0304-405X(79)90015-1
Eddelbuettel, D., & François, R. (2023). Rcpp: Seamless R and C++ integration (R package version 1.0.11). https://CRAN.R-project.org/package=Rcpp
Grolemund, G., & Wickham, H. (2017). R for data science. O’Reilly Media. https://r4ds.had.co.nz/
Hull, J. C. (2018). Options, futures, and other derivatives (10th ed.). Pearson.
Intel Corporation. (2025). Intel Corporation (INTC) company profile & facts. Yahoo Finance. https://finance.yahoo.com/quote/INTC/profile/
International Business Machines Corporation. (2025). International Business Machines Corporation (IBM) company profile & facts. Yahoo Finance. https://finance.yahoo.com/quote/IBM/profile/
Markowitz, H. (1952). Portfolio selection. The Journal of Finance, 7(1), 77-91. https://doi.org/10.2307/2975974
Pfaff, B., & McNeil, A. (2023). PortfolioAnalytics: Portfolio analysis, including numerical methods for optimization of portfolios (R package version 1.1.0). https://CRAN.R-project.org/package=PortfolioAnalytics
R Core Team. (2025). R: A language and environment for statistical computing. R Foundation for Statistical Computing. https://www.R-project.org/
Ryan, J. A., & Ulrich, J. M. (2023). quantmod: Quantitative financial modelling framework (R package version 0.4.22). https://CRAN.R-project.org/package=quantmod
Wickham, H., & Bryan, J. (2023). ggplot2: Create elegant data visualisations using the grammar of graphics (R package version 3.4.4). https://CRAN.R-project.org/package=ggplot2
Yahoo Finance. (2025). Historical stock prices: INTC, CSCO, IBM. https://finance.yahoo.com/