1. Parámetros generales

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.

2. Carga de precios históricos (2022-06-01 a 2025-03-31)

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.

3. Visualización de retornos

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.

4. Portafolio de media varianza

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.

5. Distribución de $1 millón de dólares

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.

6. Simulación MGB desde 2025-04-01 hasta 2027-04-01 (5000 simulaciones por acción)

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.

7. Análisis de Retorno, Volatilidad y VaR

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()
Análisis Trimestral del Portafolio Simulado
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.

8. Cobertura con Opciones Europeas y Americanas (Árbol Binomial)

# 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()
Opciones Europeas y Americanas - Valoración con Árbol Binomial
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.

9. Rolling trimestral de opciones Call Europeas

# 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()
Rolling Trimestral de Opciones Call Europeas
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.

10. Visualización del árbol binomial (Call Europea)

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.

11. Cobertura financiera y distribución entre activos

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.

Justificación final

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.