tick <- c("MLGO", "NCNO", "CORT")
start <- as.Date("2022-06-01")
end <- as.Date("2025-03-31")
Interpretación: Se definen los símbolos de las acciones a analizar y el rango de tiempo para obtener los datos históricos.
price_data <- tq_get(tick, from = start, to = end, get = "stock.prices")
log_ret_tidy <- price_data %>%
group_by(symbol) %>%
tq_transmute(select = adjusted, mutate_fun = periodReturn,
period = "daily", col_rename = "ret", type = "log")
log_ret_xts <- log_ret_tidy %>% spread(symbol, value = ret) %>% tk_xts()
## Warning: Non-numeric columns being dropped: date
## Using column `date` for date_var.
#Precios históricos
price_data
## # A tibble: 2,127 × 8
## symbol date open high low close volume adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 MLGO 2022-06-01 2044 2046 2042 2046 84 2046
## 2 MLGO 2022-06-02 2044 2046 2042 2046 22 2046
## 3 MLGO 2022-06-03 2044 2046 2044 2044 24 2044
## 4 MLGO 2022-06-06 2044 2046 2044 2046 30 2046
## 5 MLGO 2022-06-07 2046 2048 2046 2048 26 2048
## 6 MLGO 2022-06-08 2050 2050 2046 2048 36 2048
## 7 MLGO 2022-06-09 2050 2050 2048 2048 13 2048
## 8 MLGO 2022-06-10 2046 2048 2046 2048 91 2048
## 9 MLGO 2022-06-13 2046 2050 2044 2048 142 2048
## 10 MLGO 2022-06-14 2050 2050 2046 2050 7 2050
## # ℹ 2,117 more rows
#Retornos logaritmicos
log_ret_xts
## CORT MLGO NCNO
## 2022-06-01 0.0000000000 0.0000000000 0.000000000
## 2022-06-02 0.0124582460 0.0000000000 0.136455899
## 2022-06-03 0.0169976049 -0.0009779952 -0.021766863
## 2022-06-06 0.0028050259 0.0009779952 -0.032557766
## 2022-06-07 0.0000000000 0.0009770396 0.038113251
## 2022-06-08 0.0079051830 0.0000000000 0.006075722
## 2022-06-09 -0.0009268053 0.0000000000 -0.065419945
## 2022-06-10 -0.0074453508 0.0000000000 -0.027113230
## 2022-06-13 -0.0400244066 0.0000000000 -0.085721783
## 2022-06-14 0.0254393697 0.0009760860 0.006232611
## ...
## 2025-03-17 0.0328417040 -0.0224168576 0.012274409
## 2025-03-18 -0.0241312821 -0.0813029310 -0.006293738
## 2025-03-19 0.0262023724 -0.0248975938 0.009078283
## 2025-03-20 -0.0070940664 -0.1347326120 -0.012592003
## 2025-03-21 0.0184091168 -0.2135740712 0.003162897
## 2025-03-24 0.0137139433 1.7140838784 0.016701834
## 2025-03-25 -0.0195269218 -0.1592865106 0.013708221
## 2025-03-26 -0.0209658391 -0.0058848971 0.011842479
## 2025-03-27 -0.0075578232 0.2956602824 -0.008105407
## 2025-03-28 -0.0368382182 0.0847608813 -0.019517815
Interpretación: Se descargan los precios ajustados y se calculan los retornos logarítmicos diarios para cada acción.
ret_long <- log_ret_tidy %>% rename(Date = date, Acción = symbol, Retornos = ret)
ggplot(ret_long, aes(x = Date, y = Retornos, color = Acción)) +
geom_line() +
facet_wrap(~Acción, scales = "free_y") +
labs(title = "Retornos diarios por acción", x = "Fecha", y = "Retorno") +
theme_minimal()
Interpretación: Se visualizan los retornos diarios por acción, lo cual permite evidenciar su volatilidad y comportamiento temporal.
cov_mat <- cov(log_ret_xts) * 252
mean_ret <- colMeans(log_ret_xts)
set.seed(123)
num_port <- 5000
all_wts <- matrix(nrow = num_port, ncol = length(tick))
port_returns <- port_risk <- sharpe_ratio <- numeric(num_port)
for (i in 1:num_port) {
wts <- runif(length(tick)); wts <- wts / sum(wts)
all_wts[i, ] <- wts
port_ret <- sum(wts * mean_ret)
port_returns[i] <- (port_ret + 1)^252 - 1
port_sd <- sqrt(t(wts) %*% (cov_mat %*% wts))
port_risk[i] <- port_sd
sharpe_ratio[i] <- port_returns[i] / port_sd
}
portfolio_values <- tibble(Return = port_returns, Risk = port_risk, SharpeRatio = sharpe_ratio)
colnames(all_wts) <- tick
portfolio_values <- bind_cols(as_tibble(all_wts), portfolio_values)
media_var <- portfolio_values[which.min(portfolio_values$Risk), ]
weights_media <- as.numeric(media_var[, tick])
names(weights_media) <- tick
cov_mat
## CORT MLGO NCNO
## CORT 0.24013522 0.03001820 0.04889222
## MLGO 0.03001820 8.75499642 0.04685761
## NCNO 0.04889222 0.04685761 0.25139723
media_var
## # A tibble: 1 × 6
## MLGO NCNO CORT Return Risk SharpeRatio
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.511 0.0138 0.475 0.145 0.382 0.379
Interpretación: Se optimiza un portafolio de mínima varianza generando múltiples combinaciones de pesos y se elige aquella con menor riesgo.
investment <- 1e6
latest_prices <- price_data %>% group_by(symbol) %>% filter(date == max(date)) %>% select(symbol, adjusted)
latest_prices <- deframe(latest_prices)
shares <- investment * weights_media / latest_prices
Interpretación: Se determina la cantidad de acciones a comprar con una inversión total de USD 1 millón, de acuerdo con los pesos óptimos.
mu_annual <- mean_ret * 252
sigma_annual <- sqrt(diag(cov_mat))
set.seed(123)
Nsim <- 5000
trimestres <- 8
dt <- 0.25
sim_prices_list <- list()
for (asset in tick) {
S0 <- latest_prices[asset]
mu <- mu_annual[asset]
sigma <- sigma_annual[asset]
sim_matrix <- matrix(NA, nrow = trimestres + 1, ncol = Nsim)
sim_matrix[1, ] <- S0
for (t in 1:trimestres) {
sim_matrix[t + 1, ] <- sim_matrix[t, ] * exp((mu - 0.5 * sigma^2) * dt + sigma * sqrt(dt) * rnorm(Nsim))
}
sim_prices_list[[asset]] <- sim_matrix
}
portfolio_sim <- Reduce(`+`, Map(`*`, sim_prices_list, shares[names(sim_prices_list)]))
sim_summary <- tibble(Trimestre = 0:trimestres, Valor_Esperado = rowMeans(portfolio_sim))
Interpretación: Se simula la evolución del portafolio durante 2 años, proyectando el valor del portafolio en cada trimestre mediante MGB.
portfolio_returns_sim <- apply(portfolio_sim, 2, function(x) diff(log(x)))
portfolio_returns_sim <- t(portfolio_returns_sim)
retornos_esperados <- colMeans(portfolio_returns_sim, na.rm = TRUE)
volatilidad_esperada <- apply(portfolio_returns_sim, 2, sd, na.rm = TRUE)
VaR_1 <- apply(portfolio_returns_sim, 2, quantile, probs = 0.01, na.rm = TRUE)
VaR_5 <- apply(portfolio_returns_sim, 2, quantile, probs = 0.05, na.rm = TRUE)
valor_portafolio_inicial <- sim_summary$Valor_Esperado[1]
VaR_1_USD <- -VaR_1 * valor_portafolio_inicial
VaR_5_USD <- -VaR_5 * valor_portafolio_inicial
analisis_trimestral <- tibble(
Trimestre = 1:trimestres,
`Retorno Esperado (%)` = round(retornos_esperados * 100, 3),
`Volatilidad Esperada (%)` = round(volatilidad_esperada * 100, 3),
`VaR 1% (USD)` = round(VaR_1_USD, 2),
`VaR 5% (USD)` = round(VaR_5_USD, 2)
)
kable(analisis_trimestral, caption = "Análisis Trimestral del Portafolio Simulado") %>%
kable_styling()
| Trimestre | Retorno Esperado (%) | Volatilidad Esperada (%) | VaR 1% (USD) | VaR 5% (USD) |
|---|---|---|---|---|
| 1 | -29.888 | 46.837 | 1011016.9 | 855284.2 |
| 2 | -11.614 | 41.917 | 1334445.0 | 828858.0 |
| 3 | -3.215 | 34.654 | 1158931.1 | 601885.3 |
| 4 | 0.341 | 31.994 | 1036879.7 | 461192.9 |
| 5 | 3.357 | 28.489 | 691926.2 | 384084.9 |
| 6 | 4.711 | 26.189 | 599403.8 | 370294.7 |
| 7 | 5.046 | 24.267 | 516019.5 | 352843.9 |
| 8 | 5.099 | 24.766 | 519962.3 | 343362.8 |
Interpretación: Se evalúa el comportamiento del portafolio en términos de retorno, riesgo y pérdida máxima esperada (VaR) por trimestre.
# Función para árbol binomial de opción europea
binomial_option <- function(S, K, r, T, sigma, n, tipo = "call", americana = FALSE) {
dt <- T / n
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d)
tree <- matrix(0, nrow = n + 1, ncol = n + 1)
for (i in 0:n) tree[i + 1, n + 1] <- max(ifelse(tipo == "call", 1, -1) * (S * u^i * d^(n - i) - K), 0)
for (j in (n - 1):0) {
for (i in 0:j) {
hold <- exp(-r * dt) * (p * tree[i + 2, j + 2] + (1 - p) * tree[i + 1, j + 2])
early <- max(ifelse(tipo == "call", 1, -1) * (S * u^i * d^(j - i) - K), 0)
tree[i + 1, j + 1] <- if (americana) max(hold, early) else hold
}
}
return(tree[1, 1])
}
# Cálculo para cada acción
option_data <- tibble()
r <- 0.0372
n <- 3
T <- 2
for (asset in tick) {
S <- latest_prices[asset]
K <- S
sigma <- sigma_annual[asset]
call_euro <- binomial_option(S, K, r, T, sigma, n, tipo = "call", americana = FALSE)
call_amer <- binomial_option(S, K, r, T, sigma, n, tipo = "call", americana = TRUE)
option_data <- bind_rows(option_data, tibble(Accion = asset, Spot = S, Call_Euro = call_euro, Call_Amer = call_amer))
}
kable(option_data, caption = "Opciones Europeas y Americanas - Valoración con Árbol Binomial") %>%
kable_styling()
| Accion | Spot | Call_Euro | Call_Amer |
|---|---|---|---|
| MLGO | 17.35 | 16.712938 | 16.712938 |
| NCNO | 28.92 | 9.395043 | 9.395043 |
| CORT | 54.63 | 17.410973 | 17.410973 |
Interpretación: Se valúan las opciones europeas y americanas tipo call y put mediante el modelo binomial con 3 pasos.
# Evaluación trimestral con strike actualizado
call_rolling <- tibble()
for (t in 1:trimestres) {
for (asset in tick) {
S <- sim_summary$Valor_Esperado[t]
K <- S
sigma <- sigma_annual[asset]
call_val <- binomial_option(S, K, r, T, sigma, n, tipo = "call", americana = FALSE)
call_rolling <- bind_rows(call_rolling, tibble(Trimestre = t, Accion = asset, Valor_Call = call_val))
}
}
kable(call_rolling, caption = "Rolling Trimestral de Opciones Call Europeas") %>%
kable_styling()
| Trimestre | Accion | Valor_Call |
|---|---|---|
| 1 | MLGO | 963281.7 |
| 1 | NCNO | 324863.2 |
| 1 | CORT | 318707.2 |
| 2 | MLGO | 837002.3 |
| 2 | NCNO | 282275.9 |
| 2 | CORT | 276926.9 |
| 3 | MLGO | 751030.3 |
| 3 | NCNO | 253282.2 |
| 3 | CORT | 248482.6 |
| 4 | MLGO | 800531.2 |
| 4 | NCNO | 269976.2 |
| 4 | CORT | 264860.2 |
| 5 | MLGO | 740286.9 |
| 5 | NCNO | 249659.0 |
| 5 | CORT | 244928.1 |
| 6 | MLGO | 759803.1 |
| 6 | NCNO | 256240.8 |
| 6 | CORT | 251385.1 |
| 7 | MLGO | 803162.8 |
| 7 | NCNO | 270863.7 |
| 7 | CORT | 265730.9 |
| 8 | MLGO | 870820.1 |
| 8 | NCNO | 293680.9 |
| 8 | CORT | 288115.7 |
Interpretación: Se realiza la evaluación trimestral de opciones Call Europeas con strike ajustado al spot, para hacer cobertura dinámica.
T <- 1
n <- 3
S <- latest_prices["MLGO"]
K <- S
sigma <- sigma_annual["MLGO"]
u <- exp(sigma * sqrt(T / n))
d <- 1 / u
nodos <- expand.grid(i = 0:n, j = 0:n) %>% filter(i + j <= n)
nodos <- nodos %>% mutate(Precio = S * u^i * d^j)
nodos <- nodos %>% mutate(name = paste(i, j, sep = ","))
edges <- nodos %>% filter(i + j < n) %>% mutate(
from = name,
to1 = paste(i + 1, j, sep = ","),
to2 = paste(i, j + 1, sep = ",")
)
edges_df <- bind_rows(
data.frame(from = edges$from, to = edges$to1),
data.frame(from = edges$from, to = edges$to2)
)
vertices_df <- nodos %>% select(name, Precio) %>% distinct()
# Corregir nodos inconsistentes eliminando aristas inválidas
edges_df <- edges_df %>% filter(to %in% vertices_df$name)
graph <- graph_from_data_frame(
d = edges_df,
vertices = vertices_df,
directed = TRUE
)
ggraph(graph, layout = "tree") +
geom_edge_link() +
geom_node_label(aes(label = round(Precio, 2))) +
labs(title = "Árbol Binomial - Call Europea MLGO") +
theme_void()
Interpretación: Se visualiza la estructura del árbol binomial para una Call Europea, mostrando la evolución esperada de precios.
cobertura_pct <- 0.85
cobertura_monto <- cobertura_pct * investment
cobertura_dist <- cobertura_monto * weights_media
names(cobertura_dist) <- tick
cobertura_dist
## MLGO NCNO CORT
## 434280.2 11767.5 403952.4
Interpretación: Se define una estrategia de cobertura trimestral del 85% del portafolio, distribuida según los pesos óptimos en acciones.
Se desarrolló una estrategia de inversión diversificada usando media-varianza, simulación de precios mediante MGB y análisis de riesgo con VaR. La cobertura se realizó mediante opciones Call Europeas evaluadas con árboles binomiales, con rolling trimestral, y se definió un porcentaje fijo de cobertura con distribución proporcional. Esta metodología permite gestionar de forma activa el riesgo y proyectar el valor del portafolio con fundamentos sólidos.
¿Cómo se divide el dinero para la cobertura de las opciones?
El monto destinado a la cobertura se calculó como el 85% del total del portafolio. Luego, este valor se distribuyó entre las acciones del portafolio en proporción a los pesos de mínima varianza asignados a cada activo. Esto asegura que la cobertura sea coherente con la estructura del portafolio base.