# =================== PARÁMETROS DEL EJERCICIO ===================
capital   <- 35e6                      # USD
f_inicio  <- as.Date("2026-10-30")     # fecha inicial de inversión
f_hist    <- as.Date("2016-10-30")     # 10 años de historia
horizonte <- 4                         # años
tickers   <- c("AAPL", "AMGN", "PEP")
indice    <- "^NDX"                    # Nasdaq-100
criterio  <- "max_sharpe"              # "max_sharpe" (tangente) o "min_var"
rebalanceo <- TRUE                     # recalcular N en cada roll-over
q_ndx     <- 0.007                     # rendimiento por dividendo del NDX (supuesto)
nsim      <- 10000                     # simulaciones Monte Carlo
deriva    <- "historica"               # "historica" o "neutral" (mu = r - q, sin alfa)
retiro_exceso <- TRUE                  # retirar el saldo de margen por encima del inicial
set.seed(2026)

# Contrato de futuros: E-mini Nasdaq-100 (NQ) — CME Group
# IMPORTANTE: verificar márgenes vigentes al 30-oct-2026 en
# https://www.cmegroup.com/markets/equities/nasdaq/e-mini-nasdaq-100.margins.html
fut <- list(
  nombre         = "E-mini Nasdaq-100 (NQ) — CME Group (Globex)",
  subyacente     = "Índice Nasdaq-100 (NDX)",
  vencimientos   = "Trimestrales: marzo (H), junio (M), septiembre (U), diciembre (Z)",
  multiplicador  = 20,     # USD por punto del índice
  tick           = 0.25,   # puntos
  valor_tick     = 5,      # USD por tick (0.25 x 20)
  margen_inicial = 33000,  # USD por contrato  <-- ACTUALIZAR CON CME
  margen_mant    = 30000,  # USD por contrato  <-- ACTUALIZAR CON CME
  liquidacion    = "Financiera (cash settlement) contra la cotización especial de apertura (SOQ) del NDX el tercer viernes del mes de vencimiento",
  mtm            = "Diaria (mark-to-market)"
)
mult <- fut$multiplicador; IM <- fut$margen_inicial; MM <- fut$margen_mant
descargar <- function(t, desde, hasta, tipo = "Ad") {
  x <- getSymbols(t, src = "yahoo", from = desde, to = hasta, auto.assign = FALSE)
  if (tipo == "Ad") Ad(x) else Cl(x)
}
# Precios ajustados (incluyen dividendos y splits) -> retorno total
precios <- na.omit(do.call(merge, lapply(c(tickers, indice), descargar,
                                         desde = f_hist, hasta = f_inicio + 1)))
colnames(precios) <- c(tickers, "NDX")
# Precios de cierre (solo splits) -> evolución del precio de mercado
cierres <- na.omit(do.call(merge, lapply(tickers, descargar, desde = f_hist,
                                         hasta = f_inicio + 1, tipo = "Cl")))
colnames(cierres) <- tickers

precios_m <- precios[endpoints(precios, on = "months"), ]
ret_m     <- na.omit(diff(log(precios_m)))          # retornos logarítmicos mensuales

# Tasas libres de riesgo observadas en la fecha inicial
tasa <- function(t) {
  x <- tryCatch(getSymbols(t, src = "yahoo", from = f_inicio - 15, to = f_inicio + 1,
                           auto.assign = FALSE), error = function(e) NULL)
  if (is.null(x)) return(NA)
  v <- as.numeric(last(na.omit(Cl(x))))
  if (v > 20) v <- v / 10
  v / 100
}
rf_tnx <- tasa("^TNX"); rf_fvx <- tasa("^FVX"); rf_irx <- tasa("^IRX")
rf <- rf_tnx                                       # tasa usada (anual)
rf_c <- ifelse(is.na(rf_irx), rf_tnx, rf_irx)      # tasa corta: carry, financiamiento y descuento
P0 <- as.numeric(last(precios)); names(P0) <- colnames(precios)
S0 <- unname(P0["NDX"])

Resumen del ejercicio

Se invierten $35,000,000 el 30 de octubre de 2026 en un portafolio de tres acciones del Nasdaq-100 Apple (AAPL), Amgen (AMGN) y PepsiCo (PEP) con horizonte de 4 años. Se usan 120 meses de historia (oct-2016 a oct-2026) para optimizar el portafolio, medir su riesgo y diseñar una cobertura con futuros E-mini Nasdaq-100 (NQ) con roll-over trimestral.

ficha <- data.frame(
  Característica = c("Contrato", "Activo subyacente", "Vencimientos", "Multiplicador",
                     "Tick mínimo", "Valor del tick", "Margen inicial (por contrato)",
                     "Margen de mantenimiento (por contrato)", "Liquidación", "Mark-to-market"),
  Detalle = c(fut$nombre, fut$subyacente, fut$vencimientos, paste0(usd(mult), " × índice"),
              paste(fut$tick, "puntos"), usd(fut$valor_tick), usd(IM), usd(MM),
              fut$liquidacion, fut$mtm))
tabla(ficha, "Ficha técnica del futuro E-mini Nasdaq-100 (CME Group)")
Ficha técnica del futuro E-mini Nasdaq-100 (CME Group)
Característica Detalle
Contrato E-mini Nasdaq-100 (NQ) — CME Group (Globex)
Activo subyacente Índice Nasdaq-100 (NDX)
Vencimientos Trimestrales: marzo (H), junio (M), septiembre (U), diciembre (Z)
Multiplicador $20 × índice
Tick mínimo 0.25 puntos
Valor del tick $5
Margen inicial (por contrato) $33,000
Margen de mantenimiento (por contrato) $30,000
Liquidación Financiera (cash settlement) contra la cotización especial de apertura (SOQ) del NDX el tercer viernes del mes de vencimiento
Mark-to-market Diaria (mark-to-market)

1. Análisis fundamental

met <- function(tk) {
  x <- as.numeric(cierres[, tk]); d <- index(cierres); p0 <- tail(x, 1)
  r <- function(a) (p0 / tail(x[d <= f_inicio - round(365.25 * a)], 1))^(1 / a) - 1
  ult <- x[d > f_inicio - 365]
  c(`Precio 30-oct-2026` = usd(p0, 2), `Rend. 1 año` = pct(r(1)),
    `Rend. anual 3 años` = pct(r(3)), `Rend. anual 5 años` = pct(r(5)),
    `Máx. 52 semanas` = usd(max(ult), 2), `Mín. 52 semanas` = usd(min(ult), 2),
    `Máx. caída (10 años)` = pct(max(1 - x / cummax(x))))
}
tabla(t(sapply(tickers, met)), "Evolución reciente del precio de mercado")
Evolución reciente del precio de mercado
Precio 30-oct-2026 Rend. 1 año Rend. anual 3 años Rend. anual 5 años Máx. 52 semanas Mín. 52 semanas Máx. caída (10 años)
AAPL $344.63 26.98% 26.49% 18.13% $348.91 $246.63 38.73%
AMGN $466.01 59.59% 20.98% 17.62% $466.01 $296.30 26.72%
PEP $137.34 -6.92% -5.41% -3.20% $170.49 $123.73 36.91%
base <- merge(cierres, precios$NDX)
base100 <- sweep(coredata(base), 2, as.numeric(coredata(base)[1, ]), "/") * 100
fig1 <- plot_ly()
for (j in colnames(base))
  fig1 <- add_lines(fig1, x = index(base), y = base100[, j], name = j)
layout(fig1, title = "Precio base 100 (oct-2016 = 100)",
       yaxis = list(title = "Índice base 100", type = "log"), xaxis = list(title = ""),
       hovermode = "x unified")
# Completar con el último 10-Q / 10-K y el consenso de analistas (Investing.com, Yahoo Finance)
fund <- data.frame(
  Indicador = c("Ingresos últimos 12 meses (USD mil M)", "Margen neto", "Deuda / Patrimonio",
                "P/E (TTM)", "Precio objetivo consenso 12 m (USD)"),
  AAPL = NA, AMGN = NA, PEP = NA, check.names = FALSE)
fund[is.na(fund)] <- "Por completar"
tabla(fund, "Situación financiera (fuente: último reporte trimestral y consenso de analistas)")
Situación financiera (fuente: último reporte trimestral y consenso de analistas)
Indicador AAPL AMGN PEP
Ingresos últimos 12 meses (USD mil M) Por completar Por completar Por completar
Margen neto Por completar Por completar Por completar
Deuda / Patrimonio Por completar Por completar Por completar
P/E (TTM) Por completar Por completar Por completar
Precio objetivo consenso 12 m (USD) Por completar Por completar Por completar
# Expectativa de precio a 1 año según el modelo (log-normal con parámetros históricos)
mu_a  <- colMeans(ret_m[, tickers]) * 12
sd_a  <- apply(ret_m[, tickers], 2, sd) * sqrt(12)
P0c   <- as.numeric(last(cierres)); names(P0c) <- tickers
esper <- data.frame(
  Acción = tickers, `Precio actual` = usd(P0c, 2),
  `Precio esperado 1 año` = usd(P0c * exp(mu_a + sd_a^2 / 2), 2),
  `Intervalo 95% inferior` = usd(P0c * exp(mu_a - 1.96 * sd_a), 2),
  `Intervalo 95% superior` = usd(P0c * exp(mu_a + 1.96 * sd_a), 2), check.names = FALSE)
tabla(esper, "Expectativa de precio a 12 meses según el modelo histórico")
Expectativa de precio a 12 meses según el modelo histórico
Acción Precio actual Precio esperado 1 año Intervalo 95% inferior Intervalo 95% superior
AAPL AAPL $344.63 $462.48 $264.61 $752.81
AMGN AMGN $466.01 $555.88 $342.82 $853.81
PEP PEP $137.34 $147.20 $105.16 $200.52

Hay que tomar estos precios con cuidado. El modelo proyecta lo que pasó en los últimos diez años, que fueron muy buenos para estas acciones. Para AAPL espera USD 462 en un año, 34% por encima del precio de hoy, con un intervalo que va de 265 a 753. Si el precio objetivo de los analistas queda muy por debajo, es probable que el retorno histórico esté inflando la expectativa.

Apple (AAPL) — Tecnología / hardware y servicios

  • Negocio: diseña y vende iPhone, Mac, iPad, wearables (Apple Watch, AirPods) y servicios (App Store, iCloud, Apple Music, Apple Pay, publicidad, licencias).
  • Fuentes de ingreso: el iPhone aporta cerca de la mitad de los ingresos; Servicios es el segmento de mayor crecimiento y mayor margen, lo que vuelve más recurrente el flujo de caja.
  • Situación financiera: generación de caja muy alta, programa de recompra de acciones de gran tamaño y dividendo bajo pero creciente.
  • Por qué entra: es el motor de crecimiento del portafolio y la acción con mayor sensibilidad al índice (beta alta). Aporta rentabilidad esperada, pero concentra el riesgo sistemático que se cubrirá con futuros.

Amgen (AMGN) — Salud / biotecnología

  • Negocio: biofarmacéutica con medicamentos en hueso (Prolia, Evenity), cardiovascular (Repatha), inflamación (Enbrel, Tezspire), oncología y enfermedades raras (tras adquirir Horizon en 2023).
  • Riesgos y catalizadores: competencia de biosimilares en productos maduros frente a un pipeline relevante (incluido el candidato para obesidad MariTide).
  • Por qué entra: sector defensivo con demanda poco ligada al ciclo económico, beta baja, dividendo alto y baja correlación con Apple: reduce la varianza del portafolio.

PepsiCo (PEP) — Consumo básico / bebidas y snacks

  • Negocio: bebidas (Pepsi, Gatorade) y alimentos de conveniencia (Frito-Lay, Quaker), con fuerte peso de Norteamérica y presencia global.
  • Situación financiera: flujos estables, poder de fijación de precios y más de 50 años consecutivos aumentando el dividendo.
  • Por qué entra: acción de baja volatilidad y beta baja; amortigua las caídas del mercado y aporta ingreso por dividendos.

Lógica de la selección: La idea inicial era juntar una acción de crecimiento con beta alta (AAPL) y dos defensivas de sectores distintos, para ganar por diversificación. AMGN entró con 30.5%, por su baja correlación con Apple. PEP se quedó por fuera porque lo que aporta en diversificación no compensa su bajo retorno.


2. Retornos, volatilidad, covarianzas y correlaciones

Frecuencia: mensual (último precio ajustado de cada mes), 120 observaciones. Se eligió mensual porque el VaR y la liquidación de futuros del ejercicio son mensuales. Retorno: logarítmico, \(r_t=\ln(P_t/P_{t-1})\). Anualización: \(\mu_{anual}=12\,\bar r_{mensual}\), \(\sigma_{anual}=\sqrt{12}\,\sigma_{mensual}\), \(\Sigma_{anual}=12\,\Sigma_{mensual}\) (supone retornos independientes entre meses, por eso la varianza escala con el tiempo y la desviación con su raíz).

est <- data.frame(
  `Retorno mensual promedio` = pct(colMeans(ret_m[, tickers])),
  `Retorno anual promedio`   = pct(mu_a),
  `Desv. estándar mensual`   = pct(apply(ret_m[, tickers], 2, sd)),
  `Desv. estándar anual`     = pct(sd_a), check.names = FALSE)
tabla(est, "Estadísticas por acción")
Estadísticas por acción
Retorno mensual promedio Retorno anual promedio Desv. estándar mensual Desv. estándar anual
AAPL 2.15% 25.86% 7.70% 26.67%
AMGN 1.24% 14.93% 6.72% 23.28%
PEP 0.46% 5.58% 4.75% 16.46%
S_a <- cov(ret_m[, tickers]) * 12
tabla(round(S_a, 5), "Matriz de varianzas y covarianzas anualizada")
Matriz de varianzas y covarianzas anualizada
AAPL AMGN PEP
AAPL 0.07114 0.01311 0.01288
AMGN 0.01311 0.05419 0.01574
PEP 0.01288 0.01574 0.02711
C <- cor(ret_m[, tickers])
tabla(round(C, 4), "Matriz de correlaciones")
Matriz de correlaciones
AAPL AMGN PEP
AAPL 1.0000 0.2111 0.2934
AMGN 0.2111 1.0000 0.4107
PEP 0.2934 0.4107 1.0000
plot_ly(x = tickers, y = tickers, z = C, type = "heatmap", colorscale = "RdBu",
        zmin = -1, zmax = 1, text = round(C, 2), texttemplate = "%{text}") |>
  layout(title = "Mapa de calor de correlaciones")

AAPL tiene el retorno promedio más alto (25.9% anual), pero también es la más volátil (26.7%). PEP está en el otro extremo: 5.6% de retorno y 16.5% de volatilidad. Ninguna correlación pasa de 0.41, y la más baja es la de AAPL con AMGN (0.21). Por eso esas dos terminan juntas en el portafolio óptimo.


3. Portafolio óptimo de media-varianza

Se evalúan todas las combinaciones de pesos sin ventas en corto (\(w_i\ge 0\), \(\sum w_i=1\)) en una malla de 0.5%:

\[\mu_p=\mathbf w'\boldsymbol\mu,\qquad \sigma_p=\sqrt{\mathbf w'\Sigma\mathbf w},\qquad Sharpe=\frac{\mu_p-r_f}{\sigma_p}\]

paso <- 0.005
g <- expand.grid(a = seq(0, 1, paso), b = seq(0, 1, paso))
g <- g[g$a + g$b <= 1 + 1e-9, ]
W <- cbind(g$a, g$b, pmax(0, round(1 - g$a - g$b, 10))); colnames(W) <- tickers   # evita pesos -1e-16
mu_g <- as.numeric(W %*% mu_a)
sd_g <- sqrt(rowSums((W %*% S_a) * W))
sh_g <- (mu_g - rf) / sd_g
i_tan <- which.max(sh_g); i_mvp <- which.min(sd_g)

w <- if (criterio == "max_sharpe") W[i_tan, ] else W[i_mvp, ]
mu_p <- sum(w * mu_a); sd_p <- sqrt(as.numeric(t(w) %*% S_a %*% w))
sharpe_p <- (mu_p - rf) / sd_p

comp <- data.frame(
  Portafolio = c("Máximo Sharpe (tangente)", "Mínima varianza"),
  AAPL = pct(c(W[i_tan, 1], W[i_mvp, 1])), AMGN = pct(c(W[i_tan, 2], W[i_mvp, 2])),
  PEP = pct(c(W[i_tan, 3], W[i_mvp, 3])),
  `Retorno esperado` = pct(c(mu_g[i_tan], mu_g[i_mvp])),
  `Desv. estándar` = pct(c(sd_g[i_tan], sd_g[i_mvp])),
  Sharpe = num(c(sh_g[i_tan], sh_g[i_mvp]), 3), check.names = FALSE)
tabla(comp, paste("Portafolios candidatos — rf =", pct(rf)))
Portafolios candidatos — rf = 4.93%
Portafolio AAPL AMGN PEP Retorno esperado Desv. estándar Sharpe
Máximo Sharpe (tangente) 69.50% 30.50% 0.00% 22.52% 21.20% 0.830
Mínima varianza 16.50% 19.00% 64.50% 10.70% 15.03% 0.384
asig <- data.frame(Acción = tickers, Peso = pct(w), `Monto invertido` = usd(capital * w),
                   `Precio de compra` = usd(P0[tickers], 2),
                   `N° de acciones` = num(floor(capital * w / P0[tickers]), 0), check.names = FALSE)
tabla(asig, "Portafolio seleccionado y distribución de los USD 35 millones")
Portafolio seleccionado y distribución de los USD 35 millones
Acción Peso Monto invertido Precio de compra N° de acciones
AAPL AAPL 69.50% $24,325,000 $344.63 70,582
AMGN AMGN 30.50% $10,675,000 $466.01 22,907
PEP PEP 0.00% $0 $137.34 0

Portafolio elegido (máximo Sharpe): retorno esperado 22.52% anual, desviación estándar 21.20% y Sharpe 0.830.

set.seed(1); idx <- sample(length(mu_g), min(5000, length(mu_g)))
x_cml <- c(0, sd_g[i_tan] * 1.4)
plot_ly() |>
  add_markers(x = sd_g[idx], y = mu_g[idx], name = "Combinaciones",
              marker = list(color = sh_g[idx], colorscale = "Viridis", size = 4,
                            colorbar = list(title = "Sharpe")),
              text = sprintf("AAPL %.1f%%<br>AMGN %.1f%%<br>PEP %.1f%%",
                             100 * W[idx, 1], 100 * W[idx, 2], 100 * W[idx, 3]),
              hoverinfo = "text") |>
  add_lines(x = x_cml, y = rf + sh_g[i_tan] * x_cml, name = "Línea del mercado de capitales",
            line = list(dash = "dash", color = "gray")) |>
  add_markers(x = sqrt(diag(S_a)), y = mu_a, text = tickers, name = "Acciones",
              marker = list(size = 12, color = "black", symbol = "diamond")) |>
  add_markers(x = sd_g[i_tan], y = mu_g[i_tan], name = "Máximo Sharpe",
              marker = list(size = 15, color = "red", symbol = "star")) |>
  add_markers(x = sd_g[i_mvp], y = mu_g[i_mvp], name = "Mínima varianza",
              marker = list(size = 15, color = "orange", symbol = "star")) |>
  layout(title = "Frontera eficiente (sin ventas en corto)",
         xaxis = list(title = "Desviación estándar anual", tickformat = ".0%"),
         yaxis = list(title = "Retorno esperado anual", tickformat = ".0%"))

4. Valor en Riesgo mensual (95% y 99%)

r_p <- as.numeric(coredata(ret_m[, tickers]) %*% w)   # retorno mensual histórico del portafolio
mu_pm <- mean(r_p); sd_pm <- sd(r_p)
z <- qnorm(c(0.05, 0.01))
var_par  <- -(exp(mu_pm + z * sd_pm) - 1)
var_hist <- -(exp(quantile(r_p, c(0.05, 0.01))) - 1)
tab_var <- data.frame(
  Método = rep(c("Paramétrico (normal)", "Histórico"), each = 2),
  Confianza = rep(c("95% (VaR 5%)", "99% (VaR 1%)"), 2),
  `VaR mensual %` = pct(c(var_par, var_hist)),
  `VaR mensual USD` = usd(capital * c(var_par, var_hist)), check.names = FALSE)
tabla(tab_var, "VaR mensual del portafolio")
VaR mensual del portafolio
Método Confianza VaR mensual % VaR mensual USD
Paramétrico (normal) 95% (VaR 5%) 7.86% $2,752,654
Paramétrico (normal) 99% (VaR 1%) 11.63% $4,070,157
Histórico 95% (VaR 5%) 8.80% $3,078,403
Histórico 99% (VaR 1%) 10.69% $3,740,078
plot_ly(x = r_p, type = "histogram", nbinsx = 30, name = "Retornos mensuales") |>
  add_segments(x = log(1 - var_par[1]), xend = log(1 - var_par[1]), y = 0, yend = 20,
               name = "VaR 95%", line = list(color = "orange", width = 3)) |>
  add_segments(x = log(1 - var_par[2]), xend = log(1 - var_par[2]), y = 0, yend = 20,
               name = "VaR 99%", line = list(color = "red", width = 3)) |>
  layout(title = "Distribución histórica de retornos mensuales del portafolio",
         xaxis = list(title = "Retorno log mensual", tickformat = ".0%"), yaxis = list(title = "Frecuencia"))

Interpretación: Con el VaR paramétrico, en 95 de cada 100 meses el portafolio no debería perder más de USD 2,752,653, que es el 7.86% del capital. En el 1% de los meses más malos la pérdida podría pasar de USD 4,070,157 (11.63%).

El histórico no da lo mismo. Al 95% sale más alto (USD 3,078,400) y al 99% más bajo (USD 3,740,070). Al 99% hay que desconfiar un poco: con 120 meses, ese dato depende de uno o dos meses malos.

Este es el riesgo que el gerente quiere reducir. Pero los futuros sobre el índice solo cubren lo que se mueve con el Nasdaq-100. Si Apple tiene un mal trimestre por su cuenta, por ejemplo, eso no se cubre.


5. Betas por CAPM

\[R_{i,t}-r_{f,t}=\alpha_i+\beta_i\,(R_{NDX,t}-r_{f,t})+\varepsilon_{i,t},\qquad \beta_p=\sum_i w_i\beta_i\]

La tasa libre de riesgo histórica mensual es la letra del Tesoro a 13 semanas (^IRX).

irx   <- getSymbols("^IRX", src = "yahoo", from = f_hist, to = f_inicio + 1, auto.assign = FALSE)
irx_m <- na.omit(Cl(irx))[endpoints(na.omit(Cl(irx)), on = "months")]
rf_h  <- xts(log(1 + as.numeric(irx_m) / 100 / 12), order.by = as.yearmon(index(irx_m)))
rf_h  <- stats::lag(rf_h, 1); colnames(rf_h) <- "rf"
ret_ym <- xts(coredata(ret_m), order.by = as.yearmon(index(ret_m)))
dc <- na.omit(merge(ret_ym, rf_h))
ex <- sweep(coredata(dc[, c(tickers, "NDX")]), 1, as.numeric(dc$rf), "-")

capm <- t(sapply(tickers, function(tk) {
  m <- lm(ex[, tk] ~ ex[, "NDX"]); s <- summary(m)
  c(Alfa_mensual = coef(m)[1], Beta = coef(m)[2], EE = s$coefficients[2, 2],
    p_valor = s$coefficients[2, 4], R2 = s$r.squared)
}))
colnames(capm) <- c("Alfa mensual", "Beta", "Error estándar beta", "p-valor beta", "R²")
beta_i <- capm[, "Beta"]; beta_p <- sum(w * beta_i)
tabla(cbind(round(capm, 4), Peso = pct(w), `Beta × peso` = round(w * beta_i, 4)),
      "Regresiones CAPM contra el Nasdaq-100")
Regresiones CAPM contra el Nasdaq-100
Alfa mensual Beta Error estándar beta p-valor beta R² Peso Beta × peso
AAPL 0.0066 1.0144 0.0895 0 0.5214 69.50% 0.705
AMGN 0.0073 0.2433 0.1104 0.0295 0.0395 30.50% 0.0742
PEP -1e-04 0.2128 0.0776 0.0071 0.0599 0.00% 0
fig5 <- plot_ly()
for (tk in tickers) {
  fig5 <- add_markers(fig5, x = ex[, "NDX"], y = ex[, tk], name = tk, opacity = 0.6)
  xs <- range(ex[, "NDX"])
  fig5 <- add_lines(fig5, x = xs, y = capm[tk, 1] + beta_i[tk] * xs,
                    name = paste0(tk, " β=", round(beta_i[tk], 2)))
}
layout(fig5, title = "Línea característica de cada acción",
       xaxis = list(title = "Exceso de retorno NDX", tickformat = ".0%"),
       yaxis = list(title = "Exceso de retorno acción", tickformat = ".0%"))

Beta del portafolio: \(\beta_p=\) 0.7792. Por cada 1% que se mueva el Nasdaq-100, el portafolio tiende a moverse 0.78%.

El R² también dice bastante. El índice explica el 52% de lo que se mueve AAPL, pero solo el 4% de AMGN y el 6% de PEP. En esas dos el riesgo es casi todo de la propia empresa, y ahí los futuros sobre el índice no ayudan mucho.


6. Número óptimo de contratos

\[N^*=\beta_p\,\frac{V_P}{F_0\times \text{multiplicador}}\]

nq <- tryCatch(getSymbols("NQ=F", src = "yahoo", from = f_inicio - 10, to = f_inicio + 1,
                          auto.assign = FALSE), error = function(e) NULL)
F0_mercado <- if (!is.null(nq)) as.numeric(last(na.omit(Cl(nq)))) else NA
T0 <- as.numeric(as.Date("2026-12-18") - f_inicio) / 365     # vence NQZ26 (3er viernes)
F0_teorico <- S0 * exp((rf_c - q_ndx) * T0)                  # tasa corta, no la de 10 años
F0 <- ifelse(is.na(F0_mercado), F0_teorico, F0_mercado)
carry <- if (is.na(F0_mercado)) rf_c - q_ndx else log(F0_mercado / S0) / T0   # carry implícito en la base observada

N_exacto <- beta_p * capital / (F0 * mult)
cand <- c(floor(N_exacto), ceiling(N_exacto))
beta_res <- beta_p - cand * F0 * mult / capital
N <- cand[which.min(abs(beta_res))]

tabla(data.frame(
  Concepto = c("Nasdaq-100 spot (30-oct-2026)", "Futuro NQZ26 de mercado (NQ=F)",
               "Futuro teórico S·e^{(r−q)T}", "Precio del futuro usado F0",
               "Carry implícito (r − q)", "Valor nocional de 1 contrato", "Beta del portafolio", "N* exacto"),
  Valor = c(num(S0), num(F0_mercado), num(F0_teorico), num(F0), pct(carry, 3), usd(F0 * mult),
            num(beta_p, 4), num(N_exacto, 3))), "Insumos del cálculo")
Insumos del cálculo
Concepto Valor
Nasdaq-100 spot (30-oct-2026) 28,384.85
Futuro NQZ26 de mercado (NQ=F) 28,534.00
Futuro teórico S·e^{(r−q)T} 28,509.84
Precio del futuro usado F0 28,534.00
Carry implícito (r − q) 3.904%
Valor nocional de 1 contrato $570,680
Beta del portafolio 0.7792
N* exacto 47.789
tabla(data.frame(Redondeo = c("Hacia abajo", "Hacia arriba"), Contratos = cand,
                 `Nocional cubierto` = usd(cand * F0 * mult),
                 `Beta residual` = num(beta_res, 4), check.names = FALSE),
      "Comparación de redondeo")
Comparación de redondeo
Redondeo Contratos Nocional cubierto Beta residual
Hacia abajo 47 $26,821,960 0.0129
Hacia arriba 48 $27,392,640 -0.0034

Para el futuro teórico y el carry usamos la tasa corta (^IRX, 3.973%), no la de 10 años, porque financiar un futuro es un costo de corto plazo. Como sí había precio de mercado del NQZ26 (28,534), sacamos el carry de la base observada: ln(F0/S0)/T0 = 3.904%. Sin ese dato habríamos usado r_c − q.

Nos quedamos con 48 contratos porque es el número que deja la beta residual más cerca de cero (−0.0034; con 47 queda en 0.0129). Con 48 quedamos un poquito sobrecubiertos, es decir, ganamos algo si el mercado cae. Con 47 quedaríamos cortos de cobertura.


7. Posición en futuros: corta

Quien ya tiene las acciones y le preocupa que el mercado caiga toma una posición corta. Si el índice baja, pierde en las acciones pero gana en el futuro que vendió. La posición larga es lo contrario: la usa alguien que va a comprar acciones más adelante (o que está corto en ellas) y teme que el mercado suba antes. En ambos casos lo que se cubre es el movimiento general del Nasdaq-100, no lo que le pase a cada empresa en particular.

Con la posición corta, si el mercado sube, las acciones ganan, el futuro pierde y toca meter plata a la cuenta de margen. Si el mercado baja, lo que gana el futuro compensa lo que pierden las acciones. Al final, en los dos casos el resultado se acerca a la tasa libre de riesgo.

Decisión: el gerente ya tiene las acciones y quiere protegerse de una caída, así que vendemos 48 contratos NQ. Con ese tamaño, cada tick (0.25 puntos) vale USD 240.

mov <- c(-0.20, -0.10, -0.05, 0, 0.05, 0.10, 0.20)
dF  <- F0 * mov                                   # supone base constante
pnl_acc   <- beta_p * mov * capital
pnl_corto <- -N * mult * dF
esc <- data.frame(`Movimiento NDX` = pct(mov, 0), `Puntos` = num(dF, 1),
                  Ticks = num(dF / fut$tick, 0), `G/P acciones` = usd(pnl_acc),
                  `G/P futuro corto` = usd(pnl_corto), `G/P futuro largo` = usd(-pnl_corto),
                  `Resultado neto cubierto` = usd(pnl_acc + pnl_corto), check.names = FALSE)
tabla(esc, "Escenarios: acciones vs. futuros en ticks")
Escenarios: acciones vs. futuros en ticks
Movimiento NDX Puntos Ticks G/P acciones G/P futuro corto G/P futuro largo Resultado neto cubierto
-20% -5,706.8 -22,827 -$5,454,466 $5,478,528 -$5,478,528 $24,062
-10% -2,853.4 -11,414 -$2,727,233 $2,739,264 -$2,739,264 $12,031
-5% -1,426.7 -5,707 -$1,363,616 $1,369,632 -$1,369,632 $6,016
0% 0.0 0 $0 $0 $0 $0
5% 1,426.7 5,707 $1,363,616 -$1,369,632 $1,369,632 -$6,016
10% 2,853.4 11,414 $2,727,233 -$2,739,264 $2,739,264 -$12,031
20% 5,706.8 22,827 $5,454,466 -$5,478,528 $5,478,528 -$24,062
plot_ly(x = pct(mov, 0), y = pnl_acc, type = "bar", name = "Acciones") |>
  add_bars(y = pnl_corto, name = "Futuro corto") |>
  add_lines(y = pnl_acc + pnl_corto, name = "Neto", line = list(color = "black", width = 3)) |>
  layout(title = "La posición corta compensa el movimiento del portafolio",
         xaxis = list(title = "Movimiento del Nasdaq-100", categoryorder = "array",
                      categoryarray = pct(mov, 0)),
         yaxis = list(title = "USD"), barmode = "group")
# Cuánto del riesgo es cubrible
var_m_ndx <- var(as.numeric(ret_m$NDX))
frac_sis  <- min(1, beta_p^2 * var_m_ndx / sd_pm^2)
sd_res    <- sqrt(max(sd_pm^2 - beta_p^2 * var_m_ndx, 0))
mu_cub    <- mu_pm - beta_p * (mean(as.numeric(ret_m$NDX)) - carry / 12)   # media mensual cubierta
var_cub   <- -(exp(mu_cub + qnorm(c(0.05, 0.01)) * sd_res) - 1)            # incluye la media, comparable con var_par

En la tabla se ve que la cobertura compensa casi todo. Si el Nasdaq-100 cae 10%, las acciones pierden USD 2,727,232 y el futuro corto gana USD 2,739,264. Quedan USD 12,032 de diferencia, que vienen de estar un poco sobrecubiertos con 48 contratos.

El Nasdaq-100 explica el 48.9% de la varianza mensual del portafolio. Con la cobertura, el VaR al 95% bajaría de USD 2,752,653 a unos USD 2,111,679 (23% menos), y el VaR al 99% de USD 4,070,157 a USD 3,078,228 (24% menos). No baja más porque la otra mitad del riesgo es propia de AAPL y AMGN, y esa los futuros no la tocan. La media cubierta ya tiene en cuenta la posición corta y el carry, así que los dos VaR se pueden comparar.


8. Liquidación mensual, cuenta de margen y llamados al margen

Supuestos de la simulación

  • Se simulan 10,000 trayectorias mensuales conjuntas de las 3 acciones y el Nasdaq-100 (normal multivariada sobre retornos log, con covarianzas históricas), 48 meses. La deriva simulada es historica: “historica” usa las medias históricas (incluye el alfa de las acciones); “neutral” usa \(\mu=r_c-q\) para el índice y \(\mu=r_c\) para las acciones, sin alfa.
  • El precio del futuro en cada fecha es \(F_t=S_t\,e^{c\,(T-t)}\) con carry \(c=\) 3.904% (implícito en la base observada de NQ=F; si no hay dato, \(c=r_c-q\) con \(q=\) 0.7%); al vencimiento converge al índice.
  • Con fines académicos los vencimientos se ubican al cierre de dic, mar, jun y sep (en la realidad, el tercer viernes).
  • La cuenta inicia con \(N\times\) margen inicial. Si el saldo cae por debajo de \(N\times\) margen de mantenimiento, hay llamado al margen y se repone hasta el margen inicial. Los excedentes sobre el margen inicial se retiran en cada liquidación.
  • En cada roll el número de contratos se recalcula con las betas individuales y los valores vigentes de cada acción. La ganancia o pérdida acumulada de los futuros se capitaliza (o financia) a la tasa corta: \(G_t=G_{t-1}\,e^{r_c/12}+\Pi_t\).
  • Para las tablas se usa la trayectoria representativa: la simulación cuyo Nasdaq-100 final es la mediana.
nmes <- horizonte * 12
series <- c(tickers, "NDX")
mu_sim <- colMeans(ret_m[, series])
if (deriva == "neutral")                      # acciones a rf, NDX a rf - q (sin alfa)
  mu_sim <- c(rep(rf_c, 3), rf_c - q_ndx) / 12 - diag(cov(ret_m[, series])) / 2
set.seed(2026)                                # el set.seed(1) de la sección 3 anulaba esta semilla
Z <- MASS::mvrnorm(nsim * nmes, mu_sim, cov(ret_m[, series]))
sim <- array(Z, dim = c(nsim, nmes, 4))
precio_sim <- function(k) {
  cs <- t(apply(sim[, , k], 1, cumsum))
  cbind(P0[series[k]], P0[series[k]] * exp(cs))
}
Psim <- lapply(1:4, precio_sim)
acciones <- capital * w / P0[tickers]
V <- Reduce(`+`, lapply(1:3, function(k) acciones[k] * Psim[[k]]))   # valor portafolio
I <- Psim[[4]]                                                         # Nasdaq-100

# Calendario y contratos
fechas_ext <- c(f_inicio, seq(as.Date("2026-12-01"), by = "month", length.out = nmes + 2) - 1)
fechas <- fechas_ext[1:(nmes + 1)]
mes    <- as.integer(format(fechas, "%m"))
es_venc <- mes %in% c(3, 6, 9, 12); es_venc[1] <- FALSE
venc_vec <- c(which(es_venc), max(which(es_venc)) + 3)
contrato_de <- sapply(1:(nmes + 1), function(p) min(venc_vec[venc_vec >= p]))
venc_sig    <- sapply(1:(nmes + 1), function(p) min(venc_vec[venc_vec > p]))
cod <- function(pos) {
  f <- fechas_ext[pos]
  paste0("NQ", c(`3` = "H", `6` = "M", `9` = "U", `12` = "Z")[as.character(as.integer(format(f, "%m")))],
         format(f, "%y"))
}
# carry: definido en la sección 6 (implícito en la base observada o r_c - q)
tau <- function(v) matrix(rep((v - 1:(nmes + 1)) / 12, each = nsim), nsim)
F_liq <- I * exp(carry * tau(contrato_de))     # liquidación del contrato vigente
F_ape <- I * exp(carry * tau(venc_sig))        # precio del contrato que se abre
F_ape[, 1] <- F0
dFm <- cbind(0, F_liq[, -1] - F_ape[, -(nmes + 1)])

# Contratos vigentes en cada mes (con rebalanceo en cada roll)
N_ten <- matrix(N, nsim, nmes + 1)
for (p in 3:(nmes + 1)) {
  N_ten[, p] <- if (es_venc[p - 1] && rebalanceo)
    round(Reduce(`+`, lapply(1:3, function(k) acciones[k] * Psim[[k]][, p - 1] * beta_i[k])) /   # beta con pesos vigentes
            (F_ape[, p - 1] * mult)) else N_ten[, p - 1]
}
pnl_corta <- -N_ten * mult * dFm
G_fut <- matrix(0, nsim, nmes + 1)                 # cuenta de caja de los futuros
for (p in 2:(nmes + 1)) G_fut[, p] <- G_fut[, p - 1] * exp(rf_c / 12) + pnl_corta[, p]   # financia/capitaliza a la tasa corta
V_cub <- V + G_fut
i_rep <- which.min(abs(I[, nmes + 1] - median(I[, nmes + 1])))

liquidar_margen <- function(pnl, Nt) {
  n <- nrow(pnl); m <- ncol(pnl); saldo <- Nt[, 1] * IM
  o <- lapply(1:7, function(i) matrix(0, n, m))
  names(o) <- c("ini", "aporte", "pre", "llamado", "repo", "retiro", "fin")
  for (j in 1:m) {
    if (j > 1) {
      dep <- ifelse(Nt[, j] != Nt[, j - 1], pmax(Nt[, j] * IM - saldo, 0), 0)
      saldo <- saldo + dep; o$aporte[, j] <- dep
    }
    o$ini[, j] <- saldo; saldo <- saldo + pnl[, j]; o$pre[, j] <- saldo
    ll <- saldo < Nt[, j] * MM
    rp <- ifelse(ll, Nt[, j] * IM - saldo, 0); saldo <- saldo + rp
    o$llamado[, j] <- ll; o$repo[, j] <- rp
    rt <- if (retiro_exceso) pmax(saldo - Nt[, j] * IM, 0) else 0     # retira el excedente sobre IM
    saldo <- saldo - rt; o$retiro[, j] <- rt; o$fin[, j] <- saldo
  }
  o
}
pp  <- 2:(nmes + 1)
mg  <- liquidar_margen(pnl_corta[i_rep, pp, drop = FALSE], N_ten[i_rep, pp, drop = FALSE])
mgA <- liquidar_margen(pnl_corta[, pp], N_ten[, pp])
tab8 <- data.frame(
  Mes = 1:nmes, Fecha = format(fechas[pp], "%b-%Y"), Contrato = cod(contrato_de[pp]),
  N = N_ten[i_rep, pp], `F inicial` = F_ape[i_rep, pp - 1], `F final` = F_liq[i_rep, pp],
  `ΔF` = dFm[i_rep, pp], Ticks = round(dFm[i_rep, pp] / fut$tick),
  `G/P larga` = -pnl_corta[i_rep, pp], `G/P corta` = pnl_corta[i_rep, pp],
  `Aporte por roll` = mg$aporte[1, ], `Saldo antes del llamado` = mg$pre[1, ],
  `Llamado` = ifelse(mg$llamado[1, ] == 1, "SÍ", "No"), `Reposición` = mg$repo[1, ],
  `Retiro` = mg$retiro[1, ], `Saldo final` = mg$fin[1, ], check.names = FALSE)
datatable(tab8, rownames = FALSE, extensions = "Buttons",
          options = list(pageLength = 12, scrollX = TRUE, dom = "Bfrtip",
                         buttons = c("copy", "csv", "excel")),
          caption = "Liquidación mensual de la posición (trayectoria representativa)") |>
  formatRound(c("F inicial", "F final", "ΔF"), 2) |>
  formatCurrency(c("G/P larga", "G/P corta", "Aporte por roll", "Saldo antes del llamado",
                   "Reposición", "Retiro", "Saldo final"), digits = 0) |>
  formatStyle("Llamado", target = "row",
              backgroundColor = styleEqual(c("SÍ", "No"), c("#fdecea", "white")))
trim <- contrato_de[pp]
res8 <- data.frame(
  Contrato = cod(unique(trim)),
  `G/P posición corta` = usd(tapply(pnl_corta[i_rep, pp], trim, sum)),
  `Llamados al margen` = tapply(mg$llamado[1, ], trim, sum),
  `Reposiciones` = usd(tapply(mg$repo[1, ], trim, sum)),
  `Retiros` = usd(tapply(mg$retiro[1, ], trim, sum)),
  `Saldo al cierre` = usd(tapply(mg$fin[1, ], trim, function(x) tail(x, 1))), check.names = FALSE)
tabla(res8, "Flujos trimestrales de la posición en futuros (trayectoria representativa)")
Flujos trimestrales de la posición en futuros (trayectoria representativa)
Contrato G/P posición corta Llamados al margen Reposiciones Retiros Saldo al cierre
3 NQZ26 $3,020,839 0 $0 $3,020,839 $1,584,000
6 NQH27 -$3,799,639 2 $3,835,197 $35,557 $1,617,000
9 NQM27 -$290,339 2 $3,036,397 $2,746,058 $1,683,000
12 NQU27 -$296,022 1 $436,693 $220,703 $1,602,967
15 NQZ27 $903,307 1 $2,296,804 $3,219,078 $1,584,000
18 NQH28 -$720,913 2 $2,365,315 $1,644,402 $1,617,000
21 NQM28 -$848,451 1 $1,567,466 $719,014 $1,650,000
24 NQU28 $725,780 0 $0 $923,780 $1,452,000
27 NQZ28 -$3,374,456 2 $6,626,985 $3,252,529 $1,617,000
30 NQH29 -$3,649,805 1 $4,295,476 $645,671 $1,683,000
33 NQM29 $4,533,138 0 $0 $4,533,138 $1,914,000
36 NQU29 -$6,443,347 2 $6,443,347 $0 $2,046,000
39 NQZ29 -$10,056,645 3 $9,957,645 $0 $1,947,000
42 NQH30 -$1,857,375 2 $4,011,199 $2,186,824 $1,914,000
45 NQM30 $479,777 1 $2,899,116 $3,378,893 $2,046,000
48 NQU30 -$2,558,328 2 $7,829,079 $5,270,751 $2,046,000
51 NQZ30 -$3,382,490 1 $3,382,490 $0 $2,343,000
plot_ly(x = fechas[pp], y = mg$fin[1, ], type = "scatter", mode = "lines", name = "Saldo de margen") |>
  add_lines(y = N_ten[i_rep, pp] * MM, name = "Margen de mantenimiento", line = list(dash = "dash")) |>
  add_lines(y = N_ten[i_rep, pp] * IM, name = "Margen inicial", line = list(dash = "dot")) |>
  add_markers(x = fechas[pp][mg$llamado[1, ] == 1], y = mg$pre[1, mg$llamado[1, ] == 1],
              name = "Llamado al margen", marker = list(color = "red", size = 10)) |>
  layout(title = "Cuenta de margen", yaxis = list(title = "USD"), xaxis = list(title = ""))

En el conjunto de 10,000 simulaciones, la probabilidad de recibir al menos un llamado al margen en los 4 años es 100.0%, con un promedio de 26.5 llamados y reposiciones totales esperadas de $69,229,606, retiros esperados de $38,590,287 y un aporte neto esperado de $32,051,311. El gerente debe reservar liquidez para estos flujos: la cobertura no cuesta prima, pero sí exige caja cuando el mercado sube.


9. Roll-over trimestral (cobertura en corto)

Al vencer cada contrato se cierra la posición (liquida al precio del índice) y se abre una posición corta en el siguiente vencimiento trimestral, recalculando el número de contratos con el valor actualizado del portafolio.

pos_r <- c(which(es_venc), nmes + 1)
ini_r <- c(1, head(pos_r, -1))
tab9 <- data.frame(
  Fecha = format(fechas[pos_r], "%b-%Y"),
  `Contrato cerrado` = cod(contrato_de[pos_r]),
  `Precio de apertura` = num(F_ape[i_rep, ini_r]), `Precio de cierre` = num(F_liq[i_rep, pos_r]),
  Contratos = N_ten[i_rep, pos_r],
  `G/P realizada` = usd(sapply(seq_along(pos_r), function(k) sum(pnl_corta[i_rep, (ini_r[k] + 1):pos_r[k]]))),
  `Contrato abierto` = ifelse(pos_r <= nmes, cod(venc_sig[pos_r]), "— (fin)"),
  `Precio nuevo contrato` = ifelse(pos_r <= nmes, num(F_ape[i_rep, pos_r]), "—"),
  `Nuevos contratos` = ifelse(pos_r <= nmes, N_ten[i_rep, pmin(pos_r + 1, nmes + 1)], NA),
  `G/P acumulada` = usd(G_fut[i_rep, pos_r]),
  `Valor acciones` = usd(V[i_rep, pos_r]), `Valor cubierto` = usd(V_cub[i_rep, pos_r]),
  check.names = FALSE)
datatable(tab9, rownames = FALSE, options = list(pageLength = 17, scrollX = TRUE, dom = "t"),
          caption = "Cierre y apertura de posiciones en cada roll-over")
r_sin <- (V[, nmes + 1] / capital)^(1 / horizonte) - 1
r_con <- (pmax(V_cub[, nmes + 1], 0) / capital)^(1 / horizonte) - 1   # V_cub <= 0 es ruina (-100%), evita NaN
n_ruina       <- sum(V_cub[, nmes + 1] <= 0)
n_ruina_alguna <- sum(apply(V_cub, 1, min) <= 0)
efecto <- data.frame(
  Indicador = c("Rendimiento anual promedio", "Desviación del rendimiento anual",
                "Percentil 5% del rendimiento anual", "Probabilidad de pérdida a 4 años",
                "Rendimiento anual trayectoria representativa"),
  `Sin cobertura` = c(pct(mean(r_sin)), pct(sd(r_sin)), pct(quantile(r_sin, .05)),
                      pct(mean(V[, nmes + 1] < capital)), pct(r_sin[i_rep])),
  `Con cobertura y roll-over` = c(pct(mean(r_con)), pct(sd(r_con)), pct(quantile(r_con, .05)),
                                   pct(mean(V_cub[, nmes + 1] < capital)), pct(r_con[i_rep])),
  check.names = FALSE)
tabla(efecto, "Efecto del roll-over sobre la rentabilidad (todas las simulaciones)")
Efecto del roll-over sobre la rentabilidad (todas las simulaciones)
Indicador Sin cobertura Con cobertura y roll-over
Rendimiento anual promedio 27.68% 15.76%
Desviación del rendimiento anual 14.30% 11.88%
Percentil 5% del rendimiento anual 6.06% -1.96%
Probabilidad de pérdida a 4 años 1.16% 6.97%
Rendimiento anual trayectoria representativa 26.48% 14.77%
plot_ly(alpha = 0.55) |>
  add_histogram(x = r_sin, name = "Sin cobertura") |>
  add_histogram(x = r_con, name = "Con cobertura") |>
  layout(barmode = "overlay", title = "Distribución del rendimiento anual a 4 años",
         xaxis = list(title = "Rendimiento anual", tickformat = ".0%"))

Cuando vence un contrato, la posición se cierra al precio del índice y se vende el siguiente vencimiento a F = S e^{cτ}. Como c es positivo, el futuro nuevo se vende por encima del spot (contango), y esa diferencia se va cerrando hasta el vencimiento. Esa convergencia es ganancia para el que vende, trimestre tras trimestre. Por eso el portafolio cubierto no rinde cero, sino más o menos la tasa libre de riesgo más el alfa de las acciones. Lo que se sacrifica es la prima de riesgo del mercado.

Los números lo muestran. El rendimiento anual promedio baja de 27.72% a 15.80% y la desviación de 14.31% a 11.92%. Pero no todo mejora. El percentil 5% pasa de 5.99% a −2.30%, y la probabilidad de perder plata en cuatro años sube de 1.57% a 7.44%. Con la deriva histórica el índice sube en la mayoría de las trayectorias, y cuando sube la posición corta resta. Es decir, la cobertura reduce la dispersión pero le quita al portafolio el empuje de un mercado alcista. La trayectoria representativa lo deja claro: el Nasdaq-100 pasa de 28,385 a cerca de 58,161 puntos y la posición corta acumula una pérdida de USD 28.4 millones.

Hubo 2 trayectorias, de 10,000, en las que el valor cubierto llegó a cero o menos. A esas les asignamos pérdida total (−100%).


10. Valor esperado de la cobertura con la tasa libre de riesgo

tabla(data.frame(
  Referencia = c("^TNX — Tesoro 10 años (indicada en el taller)", "^FVX — Tesoro 5 años",
                 "^IRX — Letra 13 semanas"),
  `Tasa al 30-oct-2026` = pct(c(rf_tnx, rf_fvx, rf_irx), 3), check.names = FALSE),
  "Tasas libres de riesgo observadas")
Tasas libres de riesgo observadas
Referencia Tasa al 30-oct-2026
^TNX — Tesoro 10 años (indicada en el taller) 4.929%
^FVX — Tesoro 5 años 4.721%
^IRX — Letra 13 semanas 3.973%

El taller describe ^TNX como el índice del Tesoro de 1 a 3 años, pero en Yahoo Finance ^TNX corresponde al rendimiento del bono del Tesoro a 10 años. Se usa ^TNX como indica el docente (4.929%); como el horizonte es de 4 años, ^FVX (5 años) sería la referencia de plazo más cercana y se reporta para comparar. El carry, la capitalización de los flujos de los futuros y el descuento de su valor presente usan la tasa corta ^IRX (3.973%), porque la liquidación y el financiamiento de un futuro son de corto plazo.

En teoría, un portafolio con beta cubierta a cero no tiene riesgo sistemático y debe rendir la tasa libre de riesgo: \(E[V_t^{cub}]\approx V_0\,e^{r_f t}\).

q_pos <- 1 + 3 * (1:(nmes / 3)); t_q <- (q_pos - 1) / 12
tab10 <- data.frame(
  Trimestre = 1:length(q_pos), Fecha = format(fechas[q_pos], "%b-%Y"),
  `E[V] sin cobertura` = usd(colMeans(V[, q_pos])),
  `E[V] cubierto (simulado)` = usd(colMeans(V_cub[, q_pos])),
  `V0·e^(rf·t) teórico` = usd(capital * exp(rf * t_q)),
  `E[G/P futuros]` = usd(colMeans(G_fut[, q_pos])),
  `VP de E[G/P futuros]` = usd(colMeans(G_fut[, q_pos]) * exp(-rf_c * t_q)),
  `Desv. sin cobertura` = usd(apply(V[, q_pos], 2, sd)),
  `Desv. cubierto` = usd(apply(V_cub[, q_pos], 2, sd)), check.names = FALSE)
datatable(tab10, rownames = FALSE, options = list(pageLength = 16, scrollX = TRUE, dom = "t"),
          caption = "Valor esperado trimestral: portafolio sin cobertura vs. cubierto")
banda <- function(M, nombre, col) {
  list(x = fechas, m = colMeans(M), lo = apply(M, 2, quantile, .05),
       hi = apply(M, 2, quantile, .95), n = nombre, c = col)
}
b1 <- banda(V, "Sin cobertura", "rgba(31,119,180,0.2)")
b2 <- banda(V_cub, "Cubierto", "rgba(214,39,40,0.2)")
fig10 <- plot_ly()
for (b in list(b1, b2)) {
  fig10 <- fig10 |>
    add_ribbons(x = b$x, ymin = b$lo, ymax = b$hi, name = paste(b$n, "(5%–95%)"),
                fillcolor = b$c, line = list(width = 0)) |>
    add_lines(x = b$x, y = b$m, name = paste(b$n, "— media"))
}
fig10 |> add_lines(x = fechas, y = capital * exp(rf * (0:nmes) / 12), name = "V0·e^(rf·t)",
                   line = list(dash = "dash", color = "black")) |>
  layout(title = "Valor esperado y bandas de confianza", yaxis = list(title = "USD"))

A 4 años el valor esperado sin cobertura es $100,329,934 y el cubierto $66,863,814, frente a $42,627,861 que daría la tasa libre de riesgo. La desviación estándar del valor final baja de $48,775,640 a $28,514,117. Conclusión: la cobertura reduce el riesgo y también la rentabilidad esperada, porque se cede la prima de riesgo del mercado a cambio de estabilidad.


11. Rendimiento esperado mensual bajo tres enfoques y dividendos

r1_sin <- V[, 2] / capital - 1; r1_con <- V_cub[, 2] / capital - 1
rm_sin <- (mean(V[, nmes + 1]) / capital)^(1 / nmes) - 1
rm_con <- (mean(V_cub[, nmes + 1]) / capital)^(1 / nmes) - 1
tab11 <- data.frame(
  Enfoque = c("1. Sin cobertura (esperado)", "2. Cubierto con futuros (esperado)",
              "3a. Cuantil 5% del retorno — sin cobertura", "3b. Cuantil 1% del retorno — sin cobertura",
              "3c. Cuantil 5% del retorno — cubierto", "3d. Cuantil 1% del retorno — cubierto"),
  `Rendimiento mensual` = pct(c(mean(r1_sin), mean(r1_con), quantile(r1_sin, c(.05, .01)),
                                quantile(r1_con, c(.05, .01)))),
  `Valor a 1 mes` = usd(capital * (1 + c(mean(r1_sin), mean(r1_con), quantile(r1_sin, c(.05, .01)),
                                         quantile(r1_con, c(.05, .01))))),
  `Rend. mensual equivalente 4 años` = c(pct(rm_sin), pct(rm_con), rep("—", 4)),
  `Valor a 4 años` = usd(c(mean(V[, nmes + 1]), mean(V_cub[, nmes + 1]),
                           quantile(V[, nmes + 1], c(.05, .01)), quantile(V_cub[, nmes + 1], c(.05, .01)))),
  check.names = FALSE)
tabla(tab11, "Rendimiento y valor esperado del portafolio")
Rendimiento y valor esperado del portafolio
Enfoque Rendimiento mensual Valor a 1 mes Rend. mensual equivalente 4 años Valor a 4 años
  1. Sin cobertura (esperado)
2.17% $35,760,790 2.22% $100,329,934
  1. Cubierto con futuros (esperado)
1.11% $35,388,042 1.36% $66,863,814
3a. Cuantil 5% del retorno — sin cobertura -7.93% $32,224,240 — $44,285,495
3b. Cuantil 1% del retorno — sin cobertura -11.85% $30,852,992 — $34,279,569
3c. Cuantil 5% del retorno — cubierto -6.21% $32,824,951 — $32,332,842
3d. Cuantil 1% del retorno — cubierto -8.95% $31,866,933 — $22,888,517

Dividendos

divs <- sapply(tickers, function(tk) {
  d <- tryCatch(getDividends(tk, from = f_inicio - 365, to = f_inicio, auto.assign = FALSE),
                error = function(e) NULL)
  if (is.null(d) || NROW(d) == 0) 0 else sum(d)
})
yld <- divs / P0c
anios <- as.numeric(f_inicio - as.Date(index(precios)[1])) / 365.25
r_tot <- (as.numeric(last(precios[, tickers])) / as.numeric(precios[1, tickers]))^(1 / anios) - 1
r_pre <- (as.numeric(last(cierres)) / as.numeric(cierres[1, ]))^(1 / anios) - 1
tabla(data.frame(Acción = tickers, `Dividendo 12 m por acción` = usd(divs, 2),
                 `Rendimiento por dividendo` = pct(yld), `Dividendos anuales en el portafolio` = usd(capital * w * yld),
                 `Rend. anual solo precio (10 a.)` = pct(r_pre), `Rend. anual total (10 a.)` = pct(r_tot),
                 `Aporte del dividendo` = pct(r_tot - r_pre), check.names = FALSE),
      "Dividendos y su efecto en el rendimiento total")
Dividendos y su efecto en el rendimiento total
Acción Dividendo 12 m por acción Rendimiento por dividendo Dividendos anuales en el portafolio Rend. anual solo precio (10 a.) Rend. anual total (10 a.) Aporte del dividendo
AAPL AAPL $1.06 0.31% $74,818 28.37% 29.52% 1.15%
AMGN AMGN $9.94 2.13% $227,698 12.69% 16.10% 3.41%
PEP PEP $5.81 4.23% $0 2.51% 5.74% 3.23%

Las tres empresas pagan dividendos, por eso usamos precios ajustados: el retorno total incluye la variación del precio más el dividendo reinvertido. Con los pesos que quedaron, el portafolio recibe unos USD 302,516 al año (0.86% del capital), que es como un 0.072% mensual adicional. La que más paga en proporción es PEP (4.23%), pero como no entró, casi todo el dividendo viene de AMGN (USD 227,698).

Con la cobertura, el dueño de las acciones sigue recibiendo sus dividendos. El futuro sobre el índice ya tiene descontado el dividendo esperado del NDX en su precio, \(F=S\,e^{(r-q)T}\): entre más alto sea q, más barato el futuro y menos se gana por convergencia con la posición corta.


12. Sensibilidad a la beta: β = 0.6 y β = 1.5

bets <- c(0.6, beta_p, 1.5)
Nb <- round(bets * capital / (F0 * mult))
tab12 <- data.frame(
  Beta = c("0.60 (hipotética)", paste0(num(beta_p, 2), " (portafolio)"), "1.50 (hipotética)"),
  `N* exacto` = num(bets * capital / (F0 * mult), 2), `Contratos` = Nb,
  `Exposición sistemática` = usd(bets * capital),
  `Cambio por 1% del NDX` = usd(bets * capital * 0.01),
  `Cambio por punto del NDX` = usd(bets * capital / S0),
  `Compensa la cobertura por punto` = usd(Nb * mult),
  `Beta residual` = num(bets - Nb * F0 * mult / capital, 4), check.names = FALSE)
tabla(tab12, "Número de contratos según la beta")
Número de contratos según la beta
Beta N* exacto Contratos Exposición sistemática Cambio por 1% del NDX Cambio por punto del NDX Compensa la cobertura por punto Beta residual
0.60 (hipotética) 36.80 37 $21,000,000 $210,000 $740 $740 -0.0033
0.78 (portafolio) 47.79 48 $27,272,329 $272,723 $961 $960 -0.0034
1.50 (hipotética) 92.00 92 $52,500,000 $525,000 $1,850 $1,840 -0.0001
m <- seq(-0.25, 0.25, 0.01)
fig12 <- plot_ly()
for (k in 1:3) {
  fig12 <- fig12 |>
    add_lines(x = m, y = bets[k] * m * capital, name = paste0("β=", num(bets[k], 2), " sin cobertura"),
              line = list(dash = "dot")) |>
    add_lines(x = m, y = bets[k] * m * capital - Nb[k] * mult * F0 * m,
              name = paste0("β=", num(bets[k], 2), " cubierto"))
}
layout(fig12, title = "G/P del portafolio frente a movimientos del índice",
       xaxis = list(title = "Movimiento del Nasdaq-100", tickformat = ".0%"), yaxis = list(title = "USD"))
  • β = 0.6: el portafolio solo traslada el 60% del movimiento del índice; requiere 37 contratos. Cubrirlo con más contratos lo dejaría sobrecubierto (ganaría si el mercado cae y perdería si sube).
  • β = 1.5: amplifica 1.5 veces los movimientos del índice; requiere 92 contratos, 2.5 veces más que con β = 0.6, y mayores márgenes y llamados al margen.
  • Por qué más beta exige más cobertura: el número de contratos es proporcional a la beta. Una caída del 10% del Nasdaq-100 le resta 15% a un portafolio con β = 1.5 pero solo 10% a cada contrato vendido, así que se necesitan más contratos para igualar la pérdida.

Conclusiones

1.⁠ ⁠El portafolio de máximo Sharpe quedó con 69.5% en AAPL y 30.5% en AMGN, sin PEP. La baja correlación entre AAPL y AMGN (0.21) es lo que permite llegar a un Sharpe de 0.83.

2.⁠ ⁠El VaR mensual es USD 2,752,653 al 95% y USD 4,070,157 al 99%. Cerca del 49% de la varianza es sistemática, y esa parte es la que se puede cubrir con futuros.

3.⁠ ⁠Con β = 0.78 vendemos 48 contratos NQ. Con el roll-over trimestral, la desviación del valor final baja de USD 48.1 millones a USD 28.5 millones. El costo es que el rendimiento esperado cae de 27.7% a 15.8% anual y la probabilidad de pérdida a cuatro años sube de 1.6% a 7.4%.

4.⁠ ⁠La cobertura no cobra prima, pero pide mucha liquidez. Hubo llamados al margen en todas las simulaciones, y el aporte neto esperado ronda los USD 32 millones en cuatro años.

5.⁠ ⁠Entre más beta, más contratos y más capital para el margen.

Limitaciones: Suponemos retornos normales e independientes, lo que subestima las caídas fuertes, y parámetros históricos que no cambian. La base del futuro se modela con un costo de acarreo fijo, liquidamos cada mes y no cada día, y los vencimientos quedan al cierre de mes. Lo de la liquidación mensual se nota en la cuenta de margen. En la trayectoria representativa hay meses en que el saldo queda negativo antes del llamado (−USD 2,626,918 en octubre de 2027), algo que con liquidación diaria no pasaría porque el llamado llegaría antes.