Introducción

Este trabajo presenta una estrategia completa de inversión y cobertura para un portafolio de tres acciones del S&P 500 — NVIDIA, Tesla y Costco — con un capital de USD 20 millones y un horizonte de cuatro años a partir del 30 de abril de 2026. La estrategia integra análisis fundamental, optimización media-varianza, medición de riesgo mediante VaR, estimación de sensibilidad al mercado por CAPM, cobertura con contratos E-mini S&P 500 (ES) de CME Group y simulación prospectiva bajo tres escenarios de mercado.


1 Selección de acciones y análisis fundamental

df_id <- data.frame(
  Acción   = tickers,
  Empresa  = nombres,
  Sector   = sectores,
  Índice   = rep("S&P 500", 3),
  Dividendo = c("No distribuye", "No distribuye", "Sí — aprox. 0.6% anual"),
  row.names = NULL, check.names = FALSE
)
kable(df_id,
      caption = "Tabla 1. Identificación de las acciones seleccionadas.",
      align   = c("c","l","l","c","c")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
  column_spec(1, bold = TRUE, color = "white",
              background = c("#76b900","#E03030","#005daa"))
Tabla 1. Identificación de las acciones seleccionadas.
Acción Empresa Sector Índice Dividendo
NVDA NVIDIA Corporation Tecnología / Semiconductores / Inteligencia Artificial S&P 500 No distribuye
TSLA Tesla Inc.  Consumo discrecional / Vehículos eléctricos S&P 500 No distribuye
COST Costco Wholesale Corporation Consumo básico / Comercio minorista por membresías S&P 500 Sí — aprox. 0.6% anual

1.1 NVIDIA Corporation (NVDA)

NVIDIA Corporation diseña plataformas de cómputo acelerado, unidades de procesamiento gráfico —GPU—, soluciones de inteligencia artificial, centros de datos, software y redes especializadas. En el año fiscal 2025, la compañía registró ingresos por USD 130.5 mil millones, impulsados principalmente por el crecimiento del segmento de centros de datos, asociado a la demanda de infraestructura para inteligencia artificial y computación acelerada (NVIDIA Corporation, 2025).

El crecimiento de NVIDIA se explica por la adopción de arquitecturas como Hopper y Blackwell, utilizadas en cargas de trabajo de IA generativa, modelos de lenguaje y procesamiento intensivo de datos. Además, su ecosistema de software, incluyendo CUDA, fortalece su posición competitiva al integrar hardware y software en una plataforma de alto valor agregado (NVIDIA Corporation, 2025).

La inclusión de NVDA en el portafolio se justifica por su liderazgo en inteligencia artificial, semiconductores y centros de datos, sectores con alto potencial de crecimiento, aunque acompañados de una elevada sensibilidad a cambios en expectativas tecnológicas, demanda global y condiciones del mercado accionario.

1.2 Tesla Inc. (TSLA)

Tesla, Inc. diseña, fabrica y comercializa vehículos eléctricos, sistemas de almacenamiento de energía y soluciones relacionadas con conducción autónoma. Sus principales líneas de negocio incluyen automóviles eléctricos, generación y almacenamiento de energía, servicios, créditos regulatorios y software asociado a la conducción autónoma (Tesla, Inc., 2025).

Durante 2024, Tesla reportó ingresos totales de USD 97.69 mil millones. Aunque el segmento automotriz enfrentó presión por menores precios promedio de venta y mayor competencia, el segmento de energía mostró un crecimiento relevante, impulsado por productos como Megapack y Powerwall (Tesla, Inc., 2025).

La inclusión de TSLA en el portafolio responde a su exposición a la transición energética, vehículos eléctricos, almacenamiento de energía e innovación en inteligencia artificial aplicada a movilidad. Sin embargo, su alta volatilidad implica un mayor nivel de riesgo, por lo que complementa el portafolio como activo de crecimiento agresivo.

1.3 Costco Wholesale Corporation (COST)

Costco Wholesale Corporation opera una cadena internacional de almacenes por membresía, enfocada en la venta de productos de consumo masivo, alimentos, electrónicos, farmacia, servicios complementarios y comercio electrónico. Su modelo de negocio se basa en altos volúmenes de venta, márgenes reducidos y una fuente recurrente de ingresos proveniente de membresías (Costco Wholesale Corporation, 2025).

La compañía mantiene un perfil defensivo debido a la estabilidad de su demanda, la recurrencia de las membresías y su posición dentro del sector de consumo básico. En 2025, Costco continuó reportando una estructura sólida de ventas, expansión internacional y pago regular de dividendos, sujeto a la aprobación de su junta directiva (Costco Wholesale Corporation, 2025).

La inclusión de COST en el portafolio se justifica por su menor volatilidad relativa y su carácter defensivo frente a NVDA y TSLA. Su presencia permite equilibrar la cartera, reducir la exposición exclusiva a activos tecnológicos y aportar mayor estabilidad al rendimiento esperado.


2 Datos históricos, retornos y matrices estadísticas

Metodología aplicada:

  • Fuente de datos: Yahoo Finance — función getSymbols() del paquete quantmod. Se utiliza el precio ajustado de cierre (Adjusted Close), el cual incorpora dividendos y ajustes por splits corporativos, garantizando la consistencia de la serie histórica.
  • Frecuencia: Mensual — último precio de cierre del mes (to.monthly, indexAt = "lastof").
  • Tipo de retorno: Logarítmico continuo: \(r_t = \ln(P_t / P_{t-1})\).
  • Anualización del retorno: \((1 + \bar{r}_{mensual})^{12} - 1\)
  • Anualización de la volatilidad: \(\sigma_{mensual} \times \sqrt{12}\)
  • Anualización de la covarianza: \(Cov_{mensual} \times 12\)
  • Tratamiento de dividendos: El precio ajustado incorpora el efecto de los dividendos distribuidos por COST. Los retornos calculados representan el retorno total de la inversión, incluyendo tanto la apreciación de precio como la distribución de dividendos.
descargar_m <- function(tk, ini, fin) {
  tryCatch({
    r <- getSymbols(tk, src = "yahoo", from = ini, to = fin,
                    auto.assign = FALSE, warnings = FALSE)
    m <- to.monthly(Ad(r), indexAt = "lastof", OHLC = FALSE)
    colnames(m) <- tk; m
  }, error = function(e) NULL)
}

p_NVDA  <- descargar_m("NVDA",  fecha_hist_ini, fecha_hist_fin)
p_TSLA  <- descargar_m("TSLA",  fecha_hist_ini, fecha_hist_fin)
p_COST  <- descargar_m("COST",  fecha_hist_ini, fecha_hist_fin)
p_SP500 <- descargar_m("^GSPC", fecha_hist_ini, fecha_hist_fin)

p_ES <- tryCatch({
  r <- getSymbols("ES=F", src = "yahoo", from = fecha_hist_ini,
                  to = fecha_hist_fin, auto.assign = FALSE, warnings = FALSE)
  m <- to.monthly(Ad(r), indexAt = "lastof", OHLC = FALSE)
  colnames(m) <- "ES"; m
}, error = function(e) {
  m <- descargar_m("^GSPC", fecha_hist_ini, fecha_hist_fin)
  colnames(m) <- "ES"; m
})

precios <- merge(p_NVDA, p_TSLA, p_COST, p_SP500, p_ES, join = "inner")
colnames(precios) <- c("NVDA","TSLA","COST","SP500","ES")

F0             <- as.numeric(tail(precios[,"ES"], 1))
S0             <- as.numeric(tail(precios[,"SP500"], 1))
val_nocional   <- F0 * mult_contrato

ret_log  <- na.omit(diff(log(precios)))
ret_acc  <- ret_log[, c("NVDA","TSLA","COST")]
ret_mkt  <- ret_log[, "SP500"]

Período efectivo: 31/05/2016 a 30/04/2026 — 120 observaciones mensuales.

ret_m_med  <- colMeans(ret_acc)
ret_anual  <- (1 + ret_m_med)^fa - 1
sd_men     <- apply(ret_acc, 2, sd)
sd_anual   <- sd_men * sqrt(fa)
skew_r     <- apply(ret_acc, 2, skewness)
kurt_r     <- apply(ret_acc, 2, kurtosis)

cov_men    <- cov(ret_acc)
cov_anual  <- cov_men * fa
cor_mat    <- cor(ret_acc)

est_df <- data.frame(
  Acción            = tickers,
  `Ret. mensual %`  = round(ret_m_med*100, 4),
  `Ret. anual %`    = round(ret_anual*100, 4),
  `SD mensual %`    = round(sd_men*100, 4),
  `SD anual %`      = round(sd_anual*100, 4),
  Asimetría         = round(skew_r, 4),
  `Curtosis exceso` = round(kurt_r - 3, 4),
  check.names = FALSE, row.names = NULL
)
kable(est_df,
      caption = "Tabla 2. Estadísticas descriptivas de retornos logarítmicos mensuales (2016–2026).",
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white")
Tabla 2. Estadísticas descriptivas de retornos logarítmicos mensuales (2016–2026).
Acción Ret. mensual % Ret. anual % SD mensual % SD anual % Asimetría Curtosis exceso
NVDA 4.3753 67.1764 13.2274 45.8212 -0.4943 0.4742
TSLA 2.7066 37.7786 16.8927 58.5179 0.3334 0.4585
COST 1.7415 23.0203 6.0183 20.8479 -0.5544 0.1646

Los resultados muestran que NVDA registra el mayor retorno promedio con un rendimiento mensual de 4.3753% y anual de 67.1764%. Esto refleja su fuerte crecimiento histórico. TSLA también muestra un desempeño elevado, con un retorno anual de 37.7786%, aunque acompañado de la mayor volatilidad del grupo, con una desviación estándar anual de 58.5179%.

Por otro lado, COST muestra el perfil más defensivo, con el menor retorno anual (23.0203%) pero también con la menor volatilidad anual (20.8479%), lo que indica mayor estabilidad. En cuanto a la distribución de retornos, NVDA y COST presentan asimetría negativa, lo que sugiere mayor presencia de caídas extremas frente a subidas extremas, mientras que TSLA presenta asimetría positiva.

kable(as.data.frame(round(cov_anual, 6)),
      caption = "Tabla 3. Matriz de varianzas-covarianzas anualizada.",
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white")
Tabla 3. Matriz de varianzas-covarianzas anualizada.
NVDA TSLA COST
NVDA 0.209958 0.090097 0.039411
TSLA 0.090097 0.342434 0.037316
COST 0.039411 0.037316 0.043463

La matriz de varianzas-covarianzas anualizada evidencia que TSLA es el activo con mayor riesgo individual, al presentar la varianza más alta (0.342434), seguida por NVDA (0.209958). En contraste, COST registra la menor varianza (0.043463), lo que confirma su comportamiento más defensivo y estable dentro del portafolio.

En conclusión, esta matriz confirma que el portafolio combina activos de alto riesgo y crecimiento, como NVDA y TSLA, con un activo más defensivo como COST.

kable(as.data.frame(round(cor_mat, 4)),
      caption = "Tabla 4. Matriz de correlaciones entre retornos mensuales.",
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white")
Tabla 4. Matriz de correlaciones entre retornos mensuales.
NVDA TSLA COST
NVDA 1.0000 0.3360 0.4126
TSLA 0.3360 1.0000 0.3059
COST 0.4126 0.3059 1.0000

La matriz de correlación muestra que las tres acciones tienen correlaciones positivas moderadas, lo que significa que tienden a moverse en la misma dirección que el mercado, pero no perfectamente. Esto permite tener un portafolio diversificado, ya que la combinación de activos reduce la volatilidad del portafolio.

La menor correlación se observa entre TSLA y COST con 0.3059, lo que reflajaría una mayor diferencia en sus comportamientos financieros. Esto es coherente con sus perfiles: TSLA es una acción de alto crecimiento y mayor volatilidad, mientras que COST tiene un carácter más defensivo y estable.

También vemos que la correlación más alta se presenta entre NVDA y COST con 0.4126. Si bien sigue siendo moderada existe cierta exposición al mercado, todavía hay espacio para obtener beneficios de diversificación.

En conclusión, los resultados soportan la construcción de un portafolio diversificado, ya que las acciones no se mueven de manera idéntica. Sin embargo, al ser todas las correlaciones positivas, la diversificación reduce parcialmente el riesgo específico, pero no elimina el riesgo sistemático del mercado.

df_p <- as.data.frame(precios[, c("NVDA","TSLA","COST","SP500")])
df_p$Fecha <- index(precios)
col4 <- c("NVDA"="#76b900","TSLA"="#E03030","COST"="#005daa","SP500"="#888888")
pivot_longer(df_p, cols=-Fecha, names_to="Activo", values_to="Precio") %>%
  group_by(Activo) %>% mutate(Norm=Precio/first(Precio)*100) %>% ungroup() %>%
  ggplot(aes(x=Fecha, y=Norm, color=Activo)) +
  geom_line(size=1) + scale_color_manual(values=col4) +
  labs(title="Evolución de precios normalizados (Base 100)",
       subtitle="NVDA · TSLA · COST vs S&P 500 | Abr 2016 – Abr 2026",
       x=NULL, y="Precio normalizado", color=NULL,
       caption="Fuente: Yahoo Finance (2026). Elaboración propia.") +
  theme_minimal(base_size=12) +
  theme(plot.title=element_text(face="bold"), legend.position="bottom",
        plot.caption=element_text(color="gray50", size=9))
Figura 1. Precios normalizados Base 100. Fuente: Yahoo Finance (2026). Elaboración propia.

Figura 1. Precios normalizados Base 100. Fuente: Yahoo Finance (2026). Elaboración propia.

La gráfica en base 100 se utiliza para comparar el crecimiento relativo de diferentes activos independientemente de su precio inicial. Se fija el valor inicial en 100 y se observa cómo evoluciona en el tiempo, lo que permite interpretar el rendimiento como un índice. En esta oportunidad lo que muestra es que NVDA tuvo un crecimiento muy superior al resto, lo que explicaría su peso en el portafolio, mientras que COST y TSLA presentan un comportamiento más estable y el S&P 500 sirve como referencia del mercado.

df_r <- as.data.frame(ret_acc); df_r$Fecha <- index(ret_acc)
pivot_longer(df_r, cols=-Fecha, names_to="Acción", values_to="Retorno") %>%
  ggplot(aes(x=Fecha, y=Retorno*100, fill=Acción)) +
  geom_col(alpha=0.75, width=25) +
  geom_hline(yintercept=0, color="gray30", size=0.4) +
  facet_wrap(~Acción, ncol=1) +
  scale_fill_manual(values=colores) +
  labs(title="Retornos logarítmicos mensuales (%)",
       x=NULL, y="Retorno (%)",
       caption="Fuente: Yahoo Finance (2026). Elaboración propia.") +
  theme_minimal(base_size=11) +
  theme(plot.title=element_text(face="bold"), legend.position="none")
Figura 2. Retornos logarítmicos mensuales. Elaboración propia.

Figura 2. Retornos logarítmicos mensuales. Elaboración propia.

corrplot(cor_mat, method="color", type="upper",
         addCoef.col="black", tl.col="black", tl.srt=45,
         col=colorRampPalette(c("#d73027","white","#1a9850"))(200),
         title="Correlaciones: NVDA · TSLA · COST", mar=c(0,0,2,0))
Figura 3. Mapa de calor de correlaciones. Elaboración propia.

Figura 3. Mapa de calor de correlaciones. Elaboración propia.


3 Portafolio óptimo media-varianza

La construcción del portafolio óptimo sigue el enfoque de Markowitz (1952), que busca maximizar el Sharpe Ratio sujeto a restricciones de peso. La solución se obtiene mediante programación cuadrática con el paquete quadprog, resolviendo:

\[\max_w \frac{w'\mu - r_f}{\sqrt{w'\Sigma w}} \quad \text{s.a.} \quad \sum w_i = 1, \quad w_i \in [10\%, 70\%]\]

Las restricciones de peso \(w_i \in [10\%, 70\%]\) se aplican porque NVDA presenta retornos históricos anormalmente elevados entre 2016 y 2026, lo que sin restricciones en el portafolio llevaría al optimizador a una solución sesgada donde el 100% del capital se asignaría a un solo activo “NVDA”.

mu    <- ret_anual
Sigma <- cov_anual
n_act <- length(mu)
names(mu) <- tickers

opt_sharpe <- function(mu, Sigma, rf, wmin, wmax) {
  n    <- length(mu)
  Dmat <- 2*Sigma + diag(1e-8, n)
  dvec <- mu - rf
  Amat <- cbind(rep(1,n), diag(n), -diag(n))
  bvec <- c(1, rep(wmin,n), rep(-wmax,n))
  sol  <- solve.QP(Dmat, dvec, Amat, bvec, meq=1)
  w    <- pmax(sol$solution, 0); w/sum(w)
}

pesos      <- opt_sharpe(mu, Sigma, rf_anual, w_min, w_max)
names(pesos) <- tickers

ret_port   <- sum(pesos*mu)
sd_port    <- sqrt(as.numeric(t(pesos) %*% Sigma %*% pesos))
sharpe_p   <- (ret_port - rf_anual)/sd_port
monto_inv  <- pesos*capital
ret_port_m <- sum(pesos*ret_m_med)
sd_port_m  <- sqrt(as.numeric(t(pesos) %*% cov_men %*% pesos))

port_df <- data.frame(
  Acción          = tickers,
  Empresa         = nombres,
  `Peso (%)`      = paste0(round(pesos*100,2),"%"),
  `Monto (USD)`   = paste0("$",format(round(monto_inv,0),big.mark=",")),
  `Ret. anual %`  = paste0(round(mu*100,4),"%"),
  `SD anual %`    = paste0(round(sqrt(diag(Sigma))*100,4),"%"),
  check.names=FALSE, row.names=NULL
)
kable(port_df,
      caption="Tabla 5. Portafolio óptimo — pesos y asignación de capital.",
      align=c("c","l","c","c","c","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  column_spec(1, bold=TRUE, color="white",
              background=c("#76b900","#E03030","#005daa"))
Tabla 5. Portafolio óptimo — pesos y asignación de capital.
Acción Empresa Peso (%) Monto (USD) Ret. anual % SD anual %
NVDA NVIDIA Corporation 70% $14,000,000 67.1764% 45.8212%
TSLA Tesla Inc.  12.9% $ 2,580,128 37.7786% 58.5179%
COST Costco Wholesale Corporation 17.1% $ 3,419,872 23.0203% 20.8479%
met_df <- data.frame(
  Métrica = c("Retorno esperado anual","Retorno esperado mensual",
              "Desviación estándar anual","Desviación estándar mensual",
              "Sharpe Ratio","Tasa libre de riesgo (^TNX, 30-abr-2026)",
              "Capital total"),
  Valor = c(paste0(round(ret_port*100,4),"%"),
            paste0(round(ret_port_m*100,4),"%"),
            paste0(round(sd_port*100,4),"%"),
            paste0(round(sd_port_m*100,4),"%"),
            round(sharpe_p,4),
            paste0(rf_anual*100,"%"),
            paste0("USD ",format(capital,big.mark=","))),
  check.names=FALSE
)
kable(met_df,
      caption="Tabla 6. Métricas globales del portafolio óptimo.",
      align=c("l","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover"), full_width=FALSE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(5, bold=TRUE, background="#eaf4d3")
Tabla 6. Métricas globales del portafolio óptimo.
Métrica Valor
Retorno esperado anual 55.8335%
Retorno esperado mensual 3.7097%
Desviación estándar anual 37.0408%
Desviación estándar mensual 10.6928%
Sharpe Ratio 1.394
Tasa libre de riesgo (^TNX, 30-abr-2026) 4.2%
Capital total USD 20,000,000
data.frame(Acción=tickers, Peso=pesos*100, Monto=monto_inv/1e6) %>%
  ggplot(aes(x=reorder(Acción,-Peso), y=Peso, fill=Acción)) +
  geom_col(width=0.55, color="white") +
  geom_text(aes(label=paste0(round(Peso,2),"%\nUSD ",round(Monto,1),"M")),
            vjust=-0.3, fontface="bold", size=3.8) +
  geom_hline(yintercept=c(w_min*100,w_max*100),
             linetype="dashed", color="gray50", size=0.7) +
  scale_fill_manual(values=colores) +
  scale_y_continuous(limits=c(0,85)) +
  labs(title="Pesos óptimos — portafolio máximo Sharpe con restricciones",
       subtitle=sprintf("Ret=%.2f%% | SD=%.2f%% | Sharpe=%.4f",
                        ret_port*100, sd_port*100, sharpe_p),
       x=NULL, y="Peso (%)",
       caption="Fuente: Elaboración propia. Restricciones w ∈ [10%, 70%].") +
  theme_minimal(base_size=12) +
  theme(plot.title=element_text(face="bold"), legend.position="none")
Figura 4. Pesos óptimos del portafolio. Elaboración propia.

Figura 4. Pesos óptimos del portafolio. Elaboración propia.

Las Tablas 5 y 6 presentan el resultado del portafolio óptimo media-varianza, construido con un capital total de USD 20,000,000. La asignación final concentra el mayor peso en NVDA, con un 70% del portafolio, equivalente a USD 14,000,000. Este resultado es dado por su alto retorno histórico anual de 67.1764%, aunque también presenta una volatilidad considerable de 45.8211% anual.

En segundo lugar, el modelo asigna un 17.1% a COST, equivalente a USD 3,419,869. Aunque COST tiene el menor retorno anual esperado entre las tres acciones, con 23.0203%, también registra la menor desviación estándar anual, 20.8479%, lo que la convierte en el componente más defensivo del portafolio. Su inclusión permite reducir el riesgo total y aportar mayor estabilidad a la cartera.

También observamos que TSLA recibe el menor peso, con 12.9% que es equivalente a USD 2,580,131. A pesar de tener un retorno anual esperado elevado de 37.7786%, su desviación estándar anual es la más alta del grupo, con 58.5179%. Esto indica que TSLA ofrece potencial de rentabilidad, pero con un nivel de riesgo significativamente mayor.

A nivel global, el portafolio óptimo presenta un retorno esperado anual de 55.8335% y una desviación estándar anual de 37.0408%. Esto refleja una cartera con alto potencial de rentabilidad, pero también con una exposición importante al riesgo, especialmente por la fuerte participación de NVDA y la volatilidad de TSLA.

El Sharpe Ratio de 1.394 indica que el portafolio genera una rentabilidad significativa frente a la tasa libre de riesgo de 4.2% anual y en relación con el nivel de riesgo asumido. En pocas palablas un Sharpe positivo y superior a 1 suele interpretarse como una relación riesgo-retorno favorable, ya que el portafolio compensa al inversionista por la volatilidad y riesgo asumido.

En conclusión, el portafolio óptimo está orientado principalmente al crecimiento, liderado por NVDA, pero incorpora COST como activo estabilizador y TSLA como una acción que tiene gran potencial de retorno.

set.seed(42)
sim <- t(replicate(5000, {
  repeat {
    w <- runif(n_act); w <- w/sum(w)
    if (all(w>=w_min) && all(w<=w_max)) break
    w <- pmax(pmin(w,w_max),w_min); w <- w/sum(w); break
  }
  r <- sum(w*mu); v <- as.numeric(t(w)%*%Sigma%*%w)
  c(r, sqrt(v), (r-rf_anual)/sqrt(v))
}))
data.frame(Ret=sim[,1]*100, Risk=sim[,2]*100, SR=sim[,3]) %>%
  ggplot(aes(x=Risk, y=Ret, color=SR)) +
  geom_point(alpha=0.35, size=0.7) +
  scale_color_gradient(low="#d73027", high="#1a9850", name="Sharpe") +
  annotate("point", x=sd_port*100, y=ret_port*100,
           color="navy", size=6, shape=18) +
  annotate("label", x=sd_port*100+2.5, y=ret_port*100,
           label=sprintf("Óptimo\nNVDA=%.0f%% TSLA=%.0f%%\nCOST=%.0f%% SR=%.2f",
                         pesos[1]*100,pesos[2]*100,pesos[3]*100,sharpe_p),
           color="navy", size=2.8, fill="white", hjust=0) +
  annotate("point", x=sqrt(diag(Sigma))*100, y=mu*100,
           color=c("#76b900","#E03030","#005daa"), size=4, shape=17) +
  annotate("text", x=sqrt(diag(Sigma))*100+1.5, y=mu*100,
           label=tickers, color=c("#76b900","#E03030","#005daa"),
           size=3.5, fontface="bold") +
  labs(title="Frontera eficiente — restricciones w ∈ [10%, 70%]",
       x="Riesgo — SD anualizada (%)", y="Retorno esperado anual (%)",
       caption="Fuente: Elaboración propia con datos Yahoo Finance (2026).") +
  theme_minimal(base_size=12) +
  theme(plot.title=element_text(face="bold"))
Figura 5. Frontera eficiente — 5,000 portafolios simulados. Elaboración propia.

Figura 5. Frontera eficiente — 5,000 portafolios simulados. Elaboración propia.

la frontera eficiente confirma que el portafolio óptimo está orientado al crecimiento, principalmente por la acción de NVDA, pero incluye a COST para reducir la volatilidad total y a TSLA en una proporción limitada debido a su mayor riesgo. La combinación seleccionada maximiza la rentabilidad ajustada por riesgo y se considera eficiente frente a las demás alternativas simuladas.


4 Valor en Riesgo (VaR) mensual

El VaR cuantifica la pérdida máxima esperada del portafolio para un horizonte dado y un nivel de confianza determinado. Se presentan tres metodologías complementarias: el método paramétrico asume que los retornos siguen una distribución normal; el método histórico utiliza directamente los percentiles de la distribución empírica; el CVaR (Expected Shortfall) mide la pérdida promedio en los escenarios que superan el umbral del VaR.

ret_port_s <- as.numeric(ret_acc %*% pesos)
z99 <- qnorm(0.01); z95 <- qnorm(0.05)

Vp99p <- -(ret_port_m + z99*sd_port_m); Vp99u <- Vp99p*capital
Vp95p <- -(ret_port_m + z95*sd_port_m); Vp95u <- Vp95p*capital

rord   <- sort(ret_port_s); nobs <- length(rord)
Vh99p  <- -rord[max(1,floor(0.01*nobs))]; Vh99u <- Vh99p*capital
Vh95p  <- -rord[max(1,floor(0.05*nobs))]; Vh95u <- Vh95p*capital

CV99p  <- -mean(rord[rord <= -Vh99p]); CV99u <- CV99p*capital
CV95p  <- -mean(rord[rord <= -Vh95p]); CV95u <- CV95p*capital
var_df <- data.frame(
  Método    = c(rep("Paramétrico (Normal)",2),
                rep("Histórico",2),
                rep("CVaR / Expected Shortfall",2)),
  Confianza = rep(c("99% (VaR 1%)","95% (VaR 5%)"),3),
  `VaR (%)`  = paste0(round(c(Vp99p,Vp95p,Vh99p,Vh95p,CV99p,CV95p)*100,4),"%"),
  `VaR (USD)`= paste0("$",format(round(c(Vp99u,Vp95u,Vh99u,Vh95u,CV99u,CV95u),0),
                                  big.mark=",")),
  check.names=FALSE, row.names=NULL
)
kable(var_df,
      caption="Tabla 7. VaR mensual del portafolio — capital USD 20,000,000.",
      align=c("l","c","c","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(c(1,2), background="#fef9f0") %>%
  row_spec(c(3,4), background="#f0f7fe") %>%
  row_spec(c(5,6), background="#f0fef4")
Tabla 7. VaR mensual del portafolio — capital USD 20,000,000.
Método Confianza VaR (%) VaR (USD)
Paramétrico (Normal) 99% (VaR 1%) 21.1654% $4,233,082
Paramétrico (Normal) 95% (VaR 5%) 13.8783% $2,775,669
Histórico 99% (VaR 1%) 31.1096% $6,221,917
Histórico 95% (VaR 5%) 17.4399% $3,487,972
CVaR / Expected Shortfall 99% (VaR 1%) 31.1096% $6,221,917
CVaR / Expected Shortfall 95% (VaR 5%) 21.7766% $4,355,315

Según los resultados observados, el VaR paramétrico al 99% indica que en condiciones normales de mercado, el portafolio no debería perder más de USD 4,233,082 en un mes dado — umbral que se superaría en promedio una vez cada cien meses. El VaR al 95% fija ese límite en USD 2,775,669, con una frecuencia esperada de excedencia de cinco veces por cada cien períodos mensuales. Estos valores dimensionan la exposición al riesgo sistemático que se busca neutralizar mediante la posición de cobertura en futuros: cuando el mercado cae, las ganancias de la posición corta en futuros del indice (ES) compensan parcial o totalmente las pérdidas dentro de esos umbrales.

data.frame(Retorno=ret_port_s*100) %>%
  ggplot(aes(x=Retorno)) +
  geom_histogram(aes(y=..density..), bins=30,
                 fill="#4a90d9", color="white", alpha=0.65) +
  stat_function(fun=dnorm,
                args=list(mean=ret_port_m*100, sd=sd_port_m*100),
                color="navy", size=1.1, linetype="dashed") +
  geom_vline(xintercept=c(-Vp99p*100,-Vp95p*100),
             color=c("#d73027","#f46d43"), size=1.3) +
  geom_vline(xintercept=c(-Vh99p*100,-Vh95p*100),
             color=c("#d73027","#f46d43"), size=0.9, linetype="dashed") +
  annotate("text",
           x=c(-Vp99p*100-0.4,-Vp95p*100-0.4), y=c(0.07,0.085),
           label=c(sprintf("VaR 99%%\n-%.2f%%\n-$%sM",Vp99p*100,round(Vp99u/1e6,2)),
                   sprintf("VaR 95%%\n-%.2f%%\n-$%sM",Vp95p*100,round(Vp95u/1e6,2))),
           color=c("#d73027","#f46d43"), size=3, hjust=1) +
  labs(title="Distribución de retornos mensuales del portafolio",
       subtitle="VaR paramétrico (sólido) e histórico (punteado) | horizonte 1 mes",
       x="Retorno mensual (%)", y="Densidad",
       caption="Fuente: Elaboración propia.") +
  theme_minimal(base_size=12) +
  theme(plot.title=element_text(face="bold"))
Figura 6. Distribución de retornos mensuales del portafolio con umbrales VaR. Elaboración propia.

Figura 6. Distribución de retornos mensuales del portafolio con umbrales VaR. Elaboración propia.

La mayor parte de los retornos se concentra alrededor de valores positivos y cercanos al promedio, aunque se observa una cola izquierda relevante, lo que indica la existencia de meses con pérdidas significativas. Esta cola negativa es importante porque representa los escenarios de riesgo extremo que el VaR busca cuantificar.

En conclusión, el gráfico confirma que el portafolio tiene un perfil de rentabilidad atractivo, pero que también presenta una exposición significativa a pérdidas mensuales extremas. Por esto los resultados del VaR justifican la implementación de una estrategia de cobertura con futuros sobre el S&P 500, especialmente para mitigar esos riesgos que ocasionan las caídas fuertes del mercado.


5 Beta CAPM y exposición sistemática

El beta de cada acción se estima mediante regresión del retorno individual sobre el retorno del mercado, siguiendo el modelo de Sharpe:

\[R_i - R_f = \alpha_i + \beta_i (R_m - R_f) + \varepsilon_i\]

El beta del portafolio se obtiene como el promedio ponderado de los betas individuales, usando los pesos óptimos de la sección anterior.

exc_mkt <- as.numeric(ret_mkt) - rf_mensual

res_capm <- lapply(tickers, function(tk) {
  ea  <- as.numeric(ret_acc[,tk]) - rf_mensual
  mod <- lm(ea ~ exc_mkt); s <- summary(mod)
  list(alpha=coef(mod)[1], beta=coef(mod)[2],
       beta_se=s$coefficients[2,2], beta_p=s$coefficients[2,4],
       r2=s$r.squared)
})

betas    <- sapply(res_capm,`[[`,"beta"); names(betas) <- tickers
alphas   <- sapply(res_capm,`[[`,"alpha")
betas_se <- sapply(res_capm,`[[`,"beta_se")
r2v      <- sapply(res_capm,`[[`,"r2")
beta_pv  <- sapply(res_capm,`[[`,"beta_p")
beta_port <- sum(pesos*betas)

capm_df <- data.frame(
  Acción           = tickers,
  `Peso (%)`       = paste0(round(pesos*100,2),"%"),
  `α mensual (%)`  = round(alphas*100,5),
  `β`              = round(betas,4),
  `SE(β)`          = round(betas_se,4),
  `p-valor β`      = round(beta_pv,5),
  ``             = round(r2v,4),
  `Contribución β` = round(pesos*betas,4),
  check.names=FALSE, row.names=NULL
)
kable(capm_df,
      caption="Tabla 8. Estimación CAPM por acción — regresión mensual sobre el S&P 500.",
      align="c") %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white")
Tabla 8. Estimación CAPM por acción — regresión mensual sobre el S&P 500.
Acción Peso (%) α mensual (%) β SE(β) p-valor β Contribución β
NVDA 70% 2.75728 1.8672 0.2162 0 0.3894 1.3070
TSLA 12.9% 1.16840 1.7496 0.3141 0 0.2096 0.2257
COST 17.1% 0.85798 0.7856 0.1028 0 0.3330 0.1343
bp_df <- data.frame(
  Componente   = c(tickers,"β Portafolio"),
  `Peso (w)`   = c(paste0(round(pesos*100,2),"%"),"100%"),
  `Beta (β)`   = round(c(betas,beta_port),4),
  `w × β`      = round(c(pesos*betas,beta_port),4),
  check.names=FALSE, row.names=NULL
)
kable(bp_df,
      caption="Tabla 9. Beta del portafolio como promedio ponderado (β_p = Σ wᵢ·βᵢ).",
      align="c") %>%
  kable_styling(bootstrap_options=c("striped","hover"), full_width=FALSE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(4, bold=TRUE, background="#eaf4d3")
Tabla 9. Beta del portafolio como promedio ponderado (β_p = Σ wᵢ·βᵢ).
Componente Peso (w) Beta (β) w × β
NVDA 70% 1.8672 1.3070
TSLA 12.9% 1.7496 0.2257
COST 17.1% 0.7856 0.1343
β Portafolio 100% 1.6671 1.6671

El beta del portafolio es β_p = 1.6671. Esto indica que por cada punto porcentual de variación en el S&P 500, el portafolio se mueve en la misma dirección aproximadamente 1.67 puntos porcentuales. NVDA y TSLA aportan la mayor sensibilidad al mercado por el comportamiento en sus rendimientos, mientras que COST, con un beta cercana a 0.7 se regula el efecto en el portafolio. El resultado neto es un portafolio más agresivo y con mayor riesgo que el mercado, lo que justifica una cobertura activa mediante futuros para proteger el capital en escenarios bajistas.

ret_mkt_a <- (1+mean(as.numeric(ret_mkt)))^fa - 1
prima_mkt <- ret_mkt_a - rf_anual
sml_x  <- seq(0, max(betas)*1.25, length.out=200)
sml_df <- data.frame(Beta=sml_x, Ret=(rf_anual+sml_x*prima_mkt)*100)
pts_df <- data.frame(Acción=tickers, Beta=betas, Ret=ret_anual*100)

ggplot() +
  geom_line(data=sml_df, aes(x=Beta,y=Ret), color="navy", size=1) +
  geom_point(data=pts_df, aes(x=Beta,y=Ret,color=Acción), size=5) +
  geom_text(data=pts_df, aes(x=Beta+0.07,y=Ret,label=Acción,color=Acción),
            fontface="bold", size=4) +
  annotate("point", x=beta_port, y=ret_port*100, color="black", size=5, shape=18) +
  annotate("text", x=beta_port+0.07, y=ret_port*100,
           label=sprintf("Portafolio\nβ=%.2f",beta_port), size=3.2) +
  scale_color_manual(values=colores) +
  labs(title="Security Market Line (SML) — Modelo CAPM",
       subtitle=sprintf("Rf=%.2f%% | Prima de mercado=%.2f%%",
                        rf_anual*100, prima_mkt*100),
       x="Beta (β)", y="Retorno esperado anual (%)",
       caption="Fuente: Sharpe (1964). Datos: Yahoo Finance (2026). Elaboración propia.") +
  theme_minimal(base_size=12) +
  theme(plot.title=element_text(face="bold"), legend.position="none")
Figura 7. Security Market Line (SML). Fuente: Sharpe (1964). Datos: Yahoo Finance (2026).

Figura 7. Security Market Line (SML). Fuente: Sharpe (1964). Datos: Yahoo Finance (2026).


6 Contrato de futuros E-mini S&P 500

cont_df <- data.frame(
  Especificación = c("Denominación","Ticker CME Globex","Activo subyacente",
                     "Plataforma","Multiplicador","Vencimientos",
                     "Precio F0 (30-abr-2026)","Valor nocional por contrato",
                     "Margen inicial (10% más del mrg de mtto)","Margen de mantenimiento",
                     "Tick mínimo","Liquidación","Mark-to-market"),
  Valor = c("E-mini S&P 500","ES","S&P 500 Index","CME Globex",
            "$50 por punto del índice",
            "Trimestral: mar / jun / sep / dic",
            paste0("USD ",format(round(F0,2),big.mark=",")),
            paste0("USD ",format(round(val_nocional,0),big.mark=",")),
            paste0("USD ",format(margen_ini,big.mark=",")),
            paste0("USD ",format(margen_mant,big.mark=",")),
            "0.25 puntos = USD 12.50",
            "Cash-settled (efectivo)","Diario (académico: mensual)"),
  `Fuente (APA)` = c(rep("CME Group, 2026",6),
                     "Yahoo Finance, 2026","Cálculo propio",
                     "Cálculo propio","CME Group, 2026",
                     rep("CME Group, 2026",3)),
  check.names=FALSE, row.names=NULL
)
kable(cont_df,
      caption="Tabla 10. Especificaciones del contrato E-mini S&P 500 (ES).",
      align=c("l","l","l")) %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(c(7,8,9,10), background="#fef9f0", bold=TRUE)
Tabla 10. Especificaciones del contrato E-mini S&P 500 (ES).
Especificación Valor Fuente (APA)
Denominación E-mini S&P 500 CME Group, 2026
Ticker CME Globex ES CME Group, 2026
Activo subyacente S&P 500 Index CME Group, 2026
Plataforma CME Globex CME Group, 2026
Multiplicador $50 por punto del índice CME Group, 2026
Vencimientos Trimestral: mar / jun / sep / dic CME Group, 2026
Precio F0 (30-abr-2026) USD 7,168 Yahoo Finance, 2026
Valor nocional por contrato USD 358,400 Cálculo propio
Margen inicial (10% más del mrg de mtto) USD 26,447 Cálculo propio
Margen de mantenimiento USD 24,043 CME Group, 2026
Tick mínimo 0.25 puntos = USD 12.50 CME Group, 2026
Liquidación Cash-settled (efectivo) CME Group, 2026
Mark-to-market Diario (académico: mensual) CME Group, 2026

7 Número óptimo de contratos de futuros

\[N^* = \beta_p \times \frac{V_p}{F_0 \times Q_F}\]

Vp <- capital; QF <- mult_contrato
N_exact  <- beta_port * Vp / (F0 * QF)
N_floor  <- floor(N_exact)
N_ceil   <- ceiling(N_exact)
cob_fl   <- N_floor  * F0 * QF / Vp * 100
cob_ce   <- N_ceil   * F0 * QF / Vp * 100
N_usado  <- N_ceil
margen_total <- N_usado * margen_ini

nc_df <- data.frame(
  Parámetro = c("Beta portafolio (β_p)","Valor portafolio (Vp)",
                "Precio futuro F0","Multiplicador (QF)",
                "Valor nocional (F0 × QF)",
                "N* exacto",
                "N* redondeado hacia abajo","N* redondeado hacia arriba",
                "Margen inicial total (N↑)",
                "Cobertura efectiva (N↓)","Cobertura efectiva (N↑)"),
  Valor = c(round(beta_port,4),
            paste0("USD ",format(Vp,big.mark=",")),
            paste0("USD ",format(round(F0,2),big.mark=",")),
            paste0("$",QF," por punto"),
            paste0("USD ",format(round(F0*QF,0),big.mark=",")),
            round(N_exact,4), N_floor, N_ceil,
            paste0("USD ",format(margen_total,big.mark=",")),
            paste0(round(cob_fl,2),"%"),
            paste0(round(cob_ce,2),"%")),
  check.names=FALSE, row.names=NULL
)
kable(nc_df,
      caption="Tabla 11. Cálculo del número óptimo de contratos de futuros ES.",
      align=c("l","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover"), full_width=FALSE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(6, bold=TRUE, background="#fff3cd") %>%
  row_spec(c(7,8), bold=TRUE, background="#eaf4d3")
Tabla 11. Cálculo del número óptimo de contratos de futuros ES.
Parámetro Valor
Beta portafolio (β_p) 1.6671
Valor portafolio (Vp) USD 20,000,000
Precio futuro F0 USD 7,168
Multiplicador (QF) $50 por punto
Valor nocional (F0 × QF) USD 358,400
N* exacto 93.029
N* redondeado hacia abajo 93
N* redondeado hacia arriba 94
Margen inicial total (N↑) USD 2,486,018
Cobertura efectiva (N↓) 166.66%
Cobertura efectiva (N↑) 168.45%

para el portafolio a gestionar se toma la desición de considerar 94 contratos (redondeo hacia arriba). Debido a que la diferencia entre 93 y 94 contratos representa menos del 1% del valor nocional total, por lo que el costo de una mayor cobertura marginal es mínimo frente al beneficio de protección completa. En gestión de riesgo, cuando el objetivo es cubrir una posición larga en acciones frente a caídas del mercado, se prefiere una cobertura ligeramente superior a una menor cobertura que deje capital expuesto.


8 Posición en futuros: análisis corto vs. largo

Un inversionista que desee realizar inversiones sobre acciones y quiere protegerse frente a caídas del mercado toma una posición corta en futuros sobre el índice. La lógica es directa: si el mercado baja, el portafolio de acciones pierde valor, pero la posición corta en futuros genera una ganancia que compensa esa pérdida. Si el mercado sube, ocurre lo contrario — las acciones se valorizan, pero la posición corta genera una pérdida que reduce la ganancia neta.

pos_df <- data.frame(
  Situación = c("Mercado sube","Mercado baja",
                "Mercado sube","Mercado baja"),
  `Posición en futuros` = c("Corta","Corta","Larga","Larga"),
  `Efecto sobre portafolio acciones` = c("Gana","Pierde","Pierde","Gana"),
  `Efecto sobre posición futuros`    = c("Pierde","Gana","Gana","Pierde"),
  `Resultado neto`                   = c("Parcialmente cubierto",
                                          "Pérdida compensada",
                                          "Ganancia reducida",
                                          "Pérdida amplificada"),
  check.names=FALSE, row.names=NULL
)
kable(pos_df,
      caption="Tabla 12. Análisis de escenarios: posición corta vs. larga en futuros.",
      align=c("l","c","c","c","l")) %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(c(1,3), background="#f0fef4") %>%
  row_spec(c(2,4), background="#fef0f0")
Tabla 12. Análisis de escenarios: posición corta vs. larga en futuros.
Situación Posición en futuros Efecto sobre portafolio acciones Efecto sobre posición futuros Resultado neto
Mercado sube Corta Gana Pierde Parcialmente cubierto
Mercado baja Corta Pierde Gana Pérdida compensada
Mercado sube Larga Pierde Gana Ganancia reducida
Mercado baja Larga Gana Pierde Pérdida amplificada

Para este portafolio se adopta posición corta en 94 contratos ES. ya que el riesgo que se cubre es el riesgo sistemático del mercado, medido por la beta del portafolio (1.6671). El riesgo de cada acción no puede cubrirse con futuros sobre índice porque no está correlacionado con el movimiento del S&P 500.

La posición larga en futuros se usaría en el caso contrario: un gestor que estima recibir capital en el futuro y quiere asegurar hoy el precio de compra de acciones, o un fondo que quiere incrementar su exposición al mercado sin comprar acciones directamente. En el contexto de este ejercicio, donde ya se poseen las acciones, la posición sugerida es la corta.


9 Flujos mensuales, cuenta de margen y llamados al margen

# ── Paso 1: intentar descargar precios reales del futuro ES 2026-2030 ─────────
p_fut_raw <- tryCatch({
  r <- getSymbols("ES=F", src = "yahoo",
                  from = fecha_inv_ini,
                  to   = fecha_inv_ini + years(4),
                  auto.assign = FALSE, warnings = FALSE)
  m <- to.monthly(Ad(r), indexAt = "lastof", OHLC = FALSE)
  colnames(m) <- "ES"
  m <- na.omit(m)
  if (nrow(m) < 6) stop("pocos datos reales")
  m
}, error = function(e) NULL)

# ── Paso 2: si no hay datos reales suficientes, simular precios del futuro ────
# usando MBG calibrado con la volatilidad histórica del S&P 500 y una
# tendencia moderada (10% anual) — escenario base para la cobertura.
# Esto garantiza variación realista del saldo de margen con llamados al margen.

set.seed(42)
n_meses_cob <- 48
fechas_cob  <- seq(fecha_inv_ini %m+% months(1),
                   by = "month", length.out = n_meses_cob)

if (!is.null(p_fut_raw) && nrow(p_fut_raw) >= 6) {
  # Usar datos reales disponibles y completar con simulación el resto
  precios_fut_vec <- as.numeric(p_fut_raw[, 1])
  fechas_reales   <- index(p_fut_raw)
  n_real          <- length(precios_fut_vec)
  cat(sprintf("Datos reales ES: %d meses. Resto simulado.\n", n_real))

  # Completar con simulación desde el último precio real
  if (n_real < n_meses_cob) {
    S_last  <- tail(precios_fut_vec, 1)
    sigma_m <- sd(diff(log(precios_fut_vec))) * sqrt(12)  # vol anual
    mu_sim  <- 0.10
    dt      <- 1/12
    n_faltan <- n_meses_cob - n_real
    precios_sim <- numeric(n_faltan)
    S_cur <- S_last
    for (k in seq_len(n_faltan)) {
      S_cur        <- S_cur * exp((mu_sim - 0.5*sigma_m^2)*dt +
                                    sigma_m*sqrt(dt)*rnorm(1))
      precios_sim[k] <- S_cur
    }
    precios_fut_vec <- c(precios_fut_vec, precios_sim)
  } else {
    precios_fut_vec <- precios_fut_vec[seq_len(n_meses_cob)]
  }

} else {
  # Simulación completa: empezar desde F0 con tendencia moderada (10% anual)
  # y volatilidad histórica del S&P 500 (~18% anual), representativa del
  # comportamiento esperado del índice en el horizonte de inversión.
  cat("Usando simulación completa del precio del futuro ES (2026-2030).\n")
  sigma_sp <- as.numeric(sd_anual["COST"]) * 0.6   # proxy vol índice ~18% anual
  sigma_sp <- max(sigma_sp, 0.15); sigma_sp <- min(sigma_sp, 0.25)
  mu_fut   <- 0.10
  dt       <- 1/12

  precios_fut_vec <- numeric(n_meses_cob)
  S_cur <- F0
  for (k in seq_len(n_meses_cob)) {
    S_cur             <- S_cur * exp((mu_fut - 0.5*sigma_sp^2)*dt +
                                       sigma_sp*sqrt(dt)*rnorm(1))
    precios_fut_vec[k] <- S_cur
  }
}
## Usando simulación completa del precio del futuro ES (2026-2030).
# ── Paso 3: construir tabla_fut con precio inicial y final por mes ────────────
tabla_fut <- data.frame(
  Mes_num        = seq_len(n_meses_cob),
  Fecha          = fechas_cob,
  Precio_fin_mes = round(precios_fut_vec, 2)
) %>%
  mutate(
    Precio_ini_mes = c(F0, head(Precio_fin_mes, -1)),
    Variacion_pts  = Precio_fin_mes - Precio_ini_mes,
    GP_largo_cont  = Variacion_pts * QF,
    GP_corto_cont  = -GP_largo_cont,
    GP_largo_total = GP_largo_cont  * N_usado,
    GP_corto_total = GP_corto_cont  * N_usado
  )

# ── Paso 4: simular cuenta de margen ─────────────────────────────────────────
margen_ini_total  <- N_usado * margen_ini
margen_mant_total <- N_usado * margen_mant

saldo_antes  <- numeric(n_meses_cob)
saldo_despues <- numeric(n_meses_cob)
llamado      <- logical(n_meses_cob)
repos        <- numeric(n_meses_cob)

saldo_act <- margen_ini_total

for (i in seq_len(n_meses_cob)) {
  saldo_act      <- saldo_act + tabla_fut$GP_corto_total[i]
  saldo_antes[i] <- saldo_act          # variación real ANTES de reponer

  if (saldo_act < margen_mant_total) {
    llamado[i] <- TRUE
    repos[i]   <- margen_ini_total - saldo_act
    saldo_act  <- margen_ini_total     # se restituye al nivel inicial
  }
  saldo_despues[i] <- saldo_act
}

tabla_fut <- tabla_fut %>%
  mutate(
    Saldo_antes_repos = saldo_antes,
    Saldo_margen      = saldo_despues,
    Llamado_margen    = llamado,
    Reposicion_USD    = repos
  )

n_llamados_total <- sum(llamado)
cat(sprintf("Llamados al margen identificados: %d de %d meses\n",
            n_llamados_total, n_meses_cob))
## Llamados al margen identificados: 7 de 48 meses
# ── Tabla completa con TODOS los meses del horizonte ─────────────────────────
filas_llamado <- which(tabla_fut$Llamado_margen)

disp_df <- tabla_fut %>%
  mutate(
    Mes_label      = format(Fecha, "%b-%Y"),
    Precio_ini_mes = formatC(Precio_ini_mes, format="f", digits=2, big.mark=","),
    Precio_fin_mes = formatC(Precio_fin_mes, format="f", digits=2, big.mark=","),
    Variacion_pts  = formatC(Variacion_pts,  format="f", digits=2, big.mark=","),
    GP_corto_label = paste0(ifelse(GP_corto_total >= 0, "+", ""),
                            formatC(round(GP_corto_total,0),
                                    format="d", big.mark=",")),
    Saldo_antes_label = paste0("$", formatC(round(Saldo_antes_repos,0),
                                             format="d", big.mark=",")),
    Saldo_final_label = paste0("$", formatC(round(Saldo_margen,0),
                                             format="d", big.mark=",")),
    Llamado_label  = ifelse(Llamado_margen, "SI", "No"),
    Repos_label    = ifelse(Reposicion_USD > 0,
                            paste0("$", formatC(round(Reposicion_USD,0),
                                                format="d", big.mark=",")),
                            "-")
  ) %>%
  select(Mes_label, Precio_ini_mes, Precio_fin_mes, Variacion_pts,
         GP_corto_label, Saldo_antes_label, Saldo_final_label,
         Llamado_label, Repos_label)

kable(disp_df,
      caption = paste0("Tabla 13. Flujos mensuales completos de la posicion corta: ",
                       N_usado, " contratos ES | ", n_meses_cob,
                       " meses | ", n_llamados_total, " llamados al margen."),
      align   = c("l","r","r","r","r","r","r","c","r")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 11) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
  row_spec(filas_llamado, background = "#ffe0e0", bold = TRUE) %>%
  column_spec(8, bold = TRUE,
              color = ifelse(tabla_fut$Llamado_margen, "#a93226", "gray40")) %>%
  scroll_box(height = "480px", width = "100%")
Tabla 13. Flujos mensuales completos de la posicion corta: 94 contratos ES | 48 meses | 7 llamados al margen.
Mes_label Precio_ini_mes Precio_fin_mes Variacion_pts GP_corto_label Saldo_antes_label Saldo_final_label Llamado_label Repos_label
Jun-2026 7,168.00 7,662.87 494.87 -2,325,889 $160,129 $2,486,018 SI $2,325,889
Jul-2026 7,662.87 7,533.28 -129.59 +609,073 $3,095,091 $3,095,091 No
Aug-2026 7,533.28 7,709.48 176.20 -828,140 $2,266,951 $2,266,951 No
Sep-2026 7,709.48 7,982.49 273.01 -1,283,147 $983,804 $2,486,018 SI $1,502,214
Oct-2026 7,982.49 8,183.75 201.26 -945,922 $1,540,096 $2,486,018 SI $945,922
Nov-2026 8,183.75 8,206.70 22.95 -107,865 $2,378,153 $2,378,153 No
Dec-2026 8,206.70 8,826.85 620.15 -2,914,705 $-536,552 $2,486,018 SI $3,022,570
Jan-2027 8,826.85 8,856.00 29.15 -137,005 $2,349,013 $2,349,013 No
Feb-2027 8,856.00 9,736.59 880.59 -4,138,773 $-1,789,760 $2,486,018 SI $4,275,778
Mar-2027 9,736.59 9,782.27 45.68 -214,696 $2,271,322 $2,271,322 No
Apr-2027 9,782.27 10,427.74 645.47 -3,033,709 $-762,387 $2,486,018 SI $3,248,405
May-2027 10,427.74 11,598.55 1,170.81 -5,502,807 $-3,016,789 $2,486,018 SI $5,502,807
Jun-2027 11,598.55 11,002.66 -595.89 +2,800,683 $5,286,701 $5,286,701 No
Jul-2027 11,002.66 10,951.33 -51.33 +241,251 $5,527,952 $5,527,952 No
Aug-2027 10,951.33 10,969.11 17.78 -83,566 $5,444,386 $5,444,386 No
Sep-2027 10,969.11 11,359.07 389.96 -1,832,812 $3,611,574 $3,611,574 No
Oct-2027 11,359.07 11,303.41 -55.66 +261,602 $3,873,176 $3,873,176 No
Nov-2027 11,303.41 10,149.98 -1,153.43 +5,421,121 $9,294,297 $9,294,297 No
Dec-2027 10,149.98 9,199.90 -950.08 +4,465,376 $13,759,673 $13,759,673 No
Jan-2028 9,199.90 9,813.42 613.52 -2,883,544 $10,876,129 $10,876,129 No
Feb-2028 9,813.42 9,755.87 -57.55 +270,485 $11,146,614 $11,146,614 No
Mar-2028 9,755.87 9,098.70 -657.17 +3,088,699 $14,235,313 $14,235,313 No
Apr-2028 9,098.70 9,098.26 -0.44 +2,068 $14,237,381 $14,237,381 No
May-2028 9,098.26 9,660.80 562.54 -2,643,938 $11,593,443 $11,593,443 No
Jun-2028 9,660.80 10,564.89 904.09 -4,249,223 $7,344,220 $7,344,220 No
Jul-2028 10,564.89 10,446.76 -118.13 +555,211 $7,899,431 $7,899,431 No
Aug-2028 10,446.76 10,407.72 -39.04 +183,488 $8,082,919 $8,082,919 No
Sep-2028 10,407.72 9,714.28 -693.44 +3,259,168 $11,342,087 $11,342,087 No
Oct-2028 9,714.28 9,983.32 269.04 -1,264,488 $10,077,599 $10,077,599 No
Nov-2028 9,983.32 9,782.53 -200.79 +943,713 $11,021,312 $11,021,312 No
Dec-2028 9,782.53 10,051.44 268.91 -1,263,877 $9,757,435 $9,757,435 No
Jan-2029 10,051.44 10,439.87 388.43 -1,825,621 $7,931,814 $7,931,814 No
Feb-2029 10,439.87 10,999.49 559.62 -2,630,214 $5,301,600 $5,301,600 No
Mar-2029 10,999.49 10,792.78 -206.71 +971,537 $6,273,137 $6,273,137 No
Apr-2029 10,792.78 11,113.26 320.48 -1,506,256 $4,766,881 $4,766,881 No
May-2029 11,113.26 10,393.56 -719.70 +3,382,590 $8,149,471 $8,149,471 No
Jun-2029 10,393.56 10,121.01 -272.55 +1,280,985 $9,430,456 $9,430,456 No
Jul-2029 10,121.01 9,827.30 -293.71 +1,380,437 $10,810,893 $10,810,893 No
Aug-2029 9,827.30 8,917.56 -909.74 +4,275,778 $15,086,671 $15,086,671 No
Sep-2029 8,917.56 8,997.82 80.26 -377,222 $14,709,449 $14,709,449 No
Oct-2029 8,997.82 9,145.83 148.01 -695,647 $14,013,802 $14,013,802 No
Nov-2029 9,145.83 9,070.79 -75.04 +352,688 $14,366,490 $14,366,490 No
Dec-2029 9,070.79 9,443.10 372.31 -1,749,857 $12,616,633 $12,616,633 No
Jan-2030 9,443.10 9,218.51 -224.59 +1,055,573 $13,672,206 $13,672,206 No
Feb-2030 9,218.51 8,752.69 -465.82 +2,189,354 $15,861,560 $15,861,560 No
Mar-2030 8,752.69 8,984.47 231.78 -1,089,366 $14,772,194 $14,772,194 No
Apr-2030 8,984.47 8,738.68 -245.79 +1,155,213 $15,927,407 $15,927,407 No
May-2030 8,738.68 9,371.63 632.95 -2,974,865 $12,952,542 $12,952,542 No
# ── Tabla 13b: detalle exacto de cada llamado al margen ──────────────────────
if (n_llamados_total > 0) {
  df_llamados <- tabla_fut %>%
    filter(Llamado_margen == TRUE) %>%
    mutate(
      Mes_exact    = format(Fecha, "%B %Y"),
      F_fin        = round(Precio_fin_mes, 2),
      Saldo_previo = paste0("$", formatC(round(Saldo_antes_repos,0),
                                          format="d", big.mark=",")),
      Repos_req    = paste0("$", formatC(round(Reposicion_USD,0),
                                          format="d", big.mark=",")),
      Saldo_rest   = paste0("$", formatC(margen_ini_total,
                                          format="d", big.mark=","))
    ) %>%
    select(Mes_exact, F_fin, Saldo_previo, Repos_req, Saldo_rest)

  kable(df_llamados,
        caption = paste0("Tabla 13b. Detalle de los ", n_llamados_total,
                         " llamados al margen — mes, saldo y monto de reposición."),
        align   = c("l","c","r","r","r"),
        col.names = c("Mes del llamado","Precio futuro",
                      "Saldo antes de reponer","Monto reposición",
                      "Saldo tras reposición")) %>%
    kable_styling(bootstrap_options = c("striped","hover"),
                  full_width = FALSE) %>%
    row_spec(0, bold = TRUE, background = "#a93226", color = "white") %>%
    row_spec(seq_len(nrow(df_llamados)), background = "#ffe0e0", bold = TRUE)
} else {
  cat(paste0("Durante el horizonte de 48 meses no se registraron llamados ",
             "al margen. Esto ocurre cuando el mercado no genera movimientos ",
             "mensuales superiores a la diferencia entre margen inicial y de ",
             "mantenimiento (USD ",
             formatC(margen_ini - margen_mant, format="d", big.mark=","),
             " por contrato)."))
}
Tabla 13b. Detalle de los 7 llamados al margen — mes, saldo y monto de reposición.
Mes del llamado Precio futuro Saldo antes de reponer Monto reposición Saldo tras reposición
June 2026 7662.87 $160,129 $2,325,889 $2,486,018
September 2026 7982.49 $983,804 $1,502,214 $2,486,018
October 2026 8183.75 $1,540,096 $945,922 $2,486,018
December 2026 8826.85 $-536,552 $3,022,570 $2,486,018
February 2027 9736.59 $-1,789,760 $4,275,778 $2,486,018
April 2027 10427.74 $-762,387 $3,248,405 $2,486,018
May 2027 11598.55 $-3,016,789 $5,502,807 $2,486,018
# Construir etiquetas de eje X cada 6 meses
breaks_x <- seq(1, n_meses_cob, by = 6)
labels_x <- format(tabla_fut$Fecha[breaks_x], "%b-%y")

# Datos de llamados al margen para puntos y etiquetas
df_llamados_graf <- tabla_fut %>%
  filter(Llamado_margen == TRUE)

ggplot(tabla_fut, aes(x = Mes_num)) +
  # Área sombreada bajo la curva
  geom_ribbon(aes(ymin = margen_mant_total / 1e6,
                  ymax = Saldo_antes_repos / 1e6),
              fill = "#4a90d9", alpha = 0.12) +
  # Línea principal del saldo
  geom_line(aes(y = Saldo_antes_repos / 1e6),
            color = "#0055aa", size = 0.9) +
  # Línea margen de mantenimiento
  geom_hline(yintercept = margen_mant_total / 1e6,
             linetype = "dashed", color = "#d73027", size = 1.0) +
  # Línea margen inicial
  geom_hline(yintercept = margen_ini_total / 1e6,
             linetype = "dotted", color = "#444444", size = 0.8) +
  # Zona de peligro (entre mantenimiento y cero)
  annotate("rect",
           xmin = 1, xmax = n_meses_cob,
           ymin = 0, ymax = margen_mant_total / 1e6,
           fill = "#d73027", alpha = 0.04) +
  # Puntos de llamado al margen
  {if (nrow(df_llamados_graf) > 0)
    geom_point(data = df_llamados_graf,
               aes(x = Mes_num, y = Saldo_antes_repos / 1e6),
               color = "#d73027", size = 5, shape = 25,
               fill = "#d73027")
  } +
  # Etiquetas de llamados al margen
  {if (nrow(df_llamados_graf) > 0)
    geom_label(data = df_llamados_graf,
               aes(x = Mes_num, y = Saldo_antes_repos / 1e6,
                   label = format(Fecha, "%b\n%y")),
               color = "#a93226", size = 2.6,
               vjust = 1.8, label.size = 0.2,
               fill = "white", fontface = "bold")
  } +
  # Anotaciones de líneas de referencia (lado derecho)
  annotate("text",
           x     = n_meses_cob - 1,
           y     = margen_mant_total / 1e6 * 1.03,
           label = paste0("Marg. mant.: $",
                          formatC(round(margen_mant_total/1e3,0),
                                  format="d", big.mark=","), "K"),
           color = "#d73027", size = 3.0, hjust = 1, fontface = "bold") +
  annotate("text",
           x     = n_meses_cob - 1,
           y     = margen_ini_total / 1e6 * 1.02,
           label = paste0("Marg. inicial: $",
                          formatC(round(margen_ini_total/1e3,0),
                                  format="d", big.mark=","), "K"),
           color = "#444444", size = 3.0, hjust = 1) +
  scale_x_continuous(breaks = breaks_x, labels = labels_x) +
  scale_y_continuous(labels = function(x)
    paste0("$", formatC(x, format="f", digits=1), "M")) +
  labs(
    title    = "Cuenta de margen — posición corta en futuros ES (2026-2030)",
    subtitle = paste0(
      n_llamados_total, " llamado(s) al margen detectado(s) | ",
      "Saldo mostrado ANTES de reposición | ",
      "Banda azul = variación mensual del saldo"
    ),
    x       = NULL,
    y       = "Saldo (USD millones)",
    caption = paste0(
      "Fuente: Elaboración propia. Llamado al margen: saldo cae bajo $",
      formatC(margen_mant_total, format="d", big.mark=","),
      " (margen de mantenimiento total)."
    )
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold"),
    plot.subtitle = element_text(color = "gray40", size = 9.5),
    plot.caption  = element_text(color = "gray50", size = 8.5),
    axis.text.x   = element_text(angle = 35, hjust = 1, size = 9),
    panel.grid.minor = element_blank()
  )
Figura 8. Evolución del saldo de la cuenta de margen — posición corta (2026-2030). Elaboración propia.

Figura 8. Evolución del saldo de la cuenta de margen — posición corta (2026-2030). Elaboración propia.

Durante los primeros meses del horizonte se observan 7 llamados al margen, concentrados entre mayo de 2026 y marzo de 2027. Esto ocurre porque el saldo cae por debajo del margen de mantenimiento, lo que implica que la posición corta tuvo pérdidas por aumentos en el precio del futuro. En estos casos debemos reponer recursos hasta restablecer la cuenta al nivel del margen inicial.

A partir de 2027, el saldo de la cuenta aumenta considerablemente y se mantiene por encima de los niveles mínimos. Lo que indica que la posición corta empieza a generar ganancias acumuladas. En consecuencia, no se presentan nuevos llamados al margen durante la mayor parte del período. En conclusión, la gráfica evidencia que la cobertura con futuros requiere una gestión activa de liquidez, especialmente al inicio. Aunque la posición corta protege el portafolio ante caídas del mercado, también puede generar pérdidas temporales y llamados al margen cuando el mercado sube, por lo que es necesario mantener recursos disponibles para cumplir con las garantías exigidas.

10 Roll-over trimestral

Los contratos de futuros sobre índice tienen vencimientos trimestrales (marzo, junio, septiembre y diciembre). Para mantener la cobertura durante los cuatro años del horizonte de inversión, es necesario cerrar la posición al final de cada trimestre y abrir una nueva posición en el contrato del trimestre siguiente. Este proceso se denomina roll-over y genera ganancias o pérdidas realizadas en cada renovación.

# ── Roll-over: agrupar por trimestre sin backtick-names con caracteres especiales
tabla_roll_raw <- tabla_fut %>%
  mutate(
    anio      = lubridate::year(Fecha),
    trimestre = lubridate::quarter(Fecha),
    etiqueta  = paste0(anio, "-T", trimestre)
  ) %>%
  group_by(etiqueta) %>%
  summarise(
    F_ini_trim    = round(first(Precio_ini_mes), 2),
    F_cie_trim    = round(last(Precio_fin_mes),  2),
    GP_corta_usd  = round(sum(GP_corto_total),   2),
    GP_larga_usd  = round(sum(GP_largo_total),   2),
    n_llamados    = sum(Llamado_margen),
    repos_usd     = round(sum(Reposicion_USD),   2),
    .groups = "drop"
  ) %>%
  mutate(
    # Riesgo de base: diferencia entre F cierre del trimestre anterior
    # y F inicio del trimestre actual (precio del nuevo contrato)
    riesgo_base = round(F_ini_trim - lag(F_cie_trim), 2)
  )

# Renombrar para presentación limpia
tabla_roll <- tabla_roll_raw %>%
  rename(
    Trimestre          = etiqueta,
    `F inicio`         = F_ini_trim,
    `F cierre`         = F_cie_trim,
    `G/P corta (USD)`  = GP_corta_usd,
    `G/P larga (USD)`  = GP_larga_usd,
    `Llamados margen`  = n_llamados,
    `Reposicion (USD)` = repos_usd,
    `Riesgo de base`   = riesgo_base
  )

# Filas con resultado negativo en posición corta (mercado subió ese trimestre)
filas_neg <- which(tabla_roll$`G/P corta (USD)` < 0)
filas_pos <- which(tabla_roll$`G/P corta (USD)` >= 0)

kable(tabla_roll,
      caption = "Tabla 14. Roll-over trimestral — resultados de la posición corta.",
      align   = c("c","r","r","r","r","c","r","r")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
  row_spec(filas_pos, background = "#f0fef4") %>%
  row_spec(filas_neg, background = "#fef0f0") %>%
  column_spec(4, bold = TRUE,
              color = ifelse(tabla_roll$`G/P corta (USD)` >= 0,
                             "#0a6640", "#a93226"))
Tabla 14. Roll-over trimestral — resultados de la posición corta.
Trimestre F inicio F cierre G/P corta (USD) G/P larga (USD) Llamados margen Reposicion (USD) Riesgo de base
2026-T2 7168.00 7662.87 -2325889 2325889 1 2325889 NA
2026-T3 7662.87 7982.49 -1502214 1502214 1 1502214 0
2026-T4 7982.49 8826.85 -3968492 3968492 2 3968492 0
2027-T1 8826.85 9782.27 -4490474 4490474 1 4275778 0
2027-T2 9782.27 11002.66 -5735833 5735833 2 8751212 0
2027-T3 11002.66 11359.07 -1675127 1675127 0 0 0
2027-T4 11359.07 9199.90 10148099 -10148099 0 0 0
2028-T1 9199.90 9098.70 475640 -475640 0 0 0
2028-T2 9098.70 10564.89 -6891093 6891093 0 0 0
2028-T3 10564.89 9714.28 3997867 -3997867 0 0 0
2028-T4 9714.28 10051.44 -1584652 1584652 0 0 0
2029-T1 10051.44 10792.78 -3484298 3484298 0 0 0
2029-T2 10792.78 10121.01 3157319 -3157319 0 0 0
2029-T3 10121.01 8997.82 5278993 -5278993 0 0 0
2029-T4 8997.82 9443.10 -2092816 2092816 0 0 0
2030-T1 9443.10 8984.47 2155561 -2155561 0 0 0
2030-T2 8984.47 9371.63 -1819652 1819652 0 0 0

El riesgo de base surge porque el precio de cierre del contrato vencido y el precio de apertura del contrato entrante no siempre coinciden. Esa diferencia es la fuente de imperfección de la cobertura: si el basis se amplía en contra de la posición, el roll-over genera una pérdida adicional que no existiría si los contratos coincidieran exactamente con el horizonte de inversión.

Si el inversionista mantuviera siempre posición corta: cuando el mercado sube frecuentemente, acumularía pérdidas en la posición de futuros que reducirían la rentabilidad neta del portafolio por debajo del rendimiento de las acciones. Si mantuviera siempre posición larga: ante mercados alcistas ganaría en futuros y en acciones igualmente, pero en mercados bajistas las pérdidas se aumentarian en ambas posiciones y esto contradice el objetivo de cobertura.


11 Valor esperado de la cobertura trimestral

tasa_libre_trim  <- (1 + rf_anual)^(1/4) - 1
ret_ind_anual    <- (1 + mean(as.numeric(ret_mkt)))^fa - 1
prima_mkt_anual  <- ret_ind_anual - rf_anual
prima_mkt_trim   <- (1 + prima_mkt_anual)^(1/4) - 1
ret_port_trim    <- (1 + ret_port)^(1/4) - 1
ret_cub_trim     <- ret_port_trim - beta_port*prima_mkt_trim + tasa_libre_trim

comp_df <- data.frame(
  Enfoque = c("Sin cobertura","Con cobertura"),
  `Retorno trim. esperado` = paste0(round(c(ret_port_trim,ret_cub_trim)*100,4),"%"),
  `Valor esperado (USD)`   = paste0("$",
    format(round(capital*(1+c(ret_port_trim,ret_cub_trim)),0),big.mark=",")),
  check.names=FALSE, row.names=NULL
)
kable(comp_df,
      caption="Tabla 15. Valor esperado trimestral del portafolio con y sin cobertura.",
      align=c("l","c","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover"), full_width=FALSE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(2, background="#eaf4d3", bold=TRUE)
Tabla 15. Valor esperado trimestral del portafolio con y sin cobertura.
Enfoque Retorno trim. esperado Valor esperado (USD)
Sin cobertura 11.7288% $22,345,763
Con cobertura 9.1817% $21,836,336

Esta tabla compara el valor esperado trimestral del portafolio con y sin cobertura. El escenario sin cobertura presenta un retorno esperado mayor, de 11.7288%, equivalente a un valor proyectado de USD 22,345,763. En cambio, el portafolio con cobertura reduce el retorno esperado a 9.1817%, con un valor proyectado de USD 21,836,336.

Esta diferencia refleja el costo económico de cubrir el riesgo. la cobertura con futuros disminuye la exposición a caídas del mercado, pero también limita parte de las ganancias potenciales cuando el mercado sube. La tasa libre de riesgo utilizada corresponde al CBOE Interest Rate 3–5 Year Treasury Index (^TNX) al 30 de abril de 2026, de aproximadamente 4.2% anual(Yahoo Finance / CBOE, 2026). La cobertura no destruye valor sino que intercambia retorno esperado por reducción de riesgo, que es lo que se busca en esta estrategia.

En conclusión, la estrategia cubierta sacrifica aproximadamente 2.55 puntos porcentuales de retorno trimestral esperado a cambio de una mayor protección frente a escenarios adversos.


12 Rendimiento esperado: tres enfoques y efecto de dividendos

La tabla compara el valor esperado mensual del portafolio bajo cuatro enfoques de riesgo y cobertura. El portafolio sin cobertura presenta el mayor retorno esperado, con 3.7097% mensual, equivalente a USD 20,741,939, reflejando el rendimiento esperado sin reducir la exposición al mercado.

Al incorporar la cobertura con futuros, el retorno esperado disminuye a 2.921%, con un valor proyectado de USD 20,584,195. Esta reducción es consistente con el objetivo de la cobertura: proteger el portafolio frente a caídas del mercado, aunque sacrificando parte de la rentabilidad esperada.

ret_m_med_port   <- mean(ret_port_s)
prima_mkt_m      <- mean(as.numeric(ret_mkt)) - rf_mensual
tasa_libre_m_eff <- (1 + rf_anual)^(1/12) - 1
ret_cub_m        <- ret_m_med_port - beta_port*prima_mkt_m + tasa_libre_m_eff

enf_df <- data.frame(
  Enfoque = c("Sin cobertura",
              "Cubierto con futuros",
              "Ajustado por VaR 95%",
              "Ajustado por VaR 99%"),
  `Retorno mensual` = paste0(round(c(ret_m_med_port,
                                     ret_cub_m,
                                     ret_m_med_port - Vp95p,
                                     ret_m_med_port - Vp99p)*100,4),"%"),
  `Valor esperado (USD)` = paste0("$",
    format(round(capital*(1+c(ret_m_med_port,
                              ret_cub_m,
                              ret_m_med_port - Vp95p,
                              ret_m_med_port - Vp99p)),0),
           big.mark=",")),
  check.names=FALSE, row.names=NULL
)
kable(enf_df,
      caption="Tabla 16. Valor esperado mensual del portafolio bajo tres enfoques.",
      align=c("l","c","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(1, background="#f0f7fe") %>%
  row_spec(2, background="#eaf4d3") %>%
  row_spec(c(3,4), background="#fef9f0")
Tabla 16. Valor esperado mensual del portafolio bajo tres enfoques.
Enfoque Retorno mensual Valor esperado (USD)
Sin cobertura 3.7097% $20,741,939
Cubierto con futuros 2.921% $20,584,195
Ajustado por VaR 95% -10.1687% $17,966,270
Ajustado por VaR 99% -17.4557% $16,508,857

Efecto de dividendos: COST distribuye un dividendo regular de aproximadamente 0.6% anual, equivalente a unos USD 0.15 por acción cada trimestre. Los precios ajustados utilizados en los cálculos ya incorporan este rendimiento, de modo que los retornos representan el retorno total de la inversión (apreciación de precio más dividendos). Para NVDA y TSLA, que no distribuyen dividendos en el período analizado, el precio ajustado coincide con el precio de cierre. El dividendo de COST mejora marginalmente el retorno total del portafolio respecto a un portafolio compuesto exclusivamente por acciones de crecimiento.

12.1 Simulación de escenarios a 48 meses

Para complementar el análisis puntual, se simula la evolución del portafolio a lo largo del horizonte de inversión bajo tres escenarios de mercado mediante el modelo de Black-Scholes-Merton de movimiento browniano geométrico:

\[S_t = S_0 \exp\left[\left(\mu - \frac{\sigma^2}{2}\right)t + \sigma \sqrt{t}\, Z\right], \quad Z \sim N(0,1)\]

# ── Función de simulación MBG (movimiento browniano geométrico) ───────────────
simular_trayectorias <- function(S0, mu_a, sigma_a, meses = 48, n = 1000) {
  set.seed(123)
  dt  <- 1 / 12
  M   <- matrix(NA, nrow = meses + 1, ncol = n)
  M[1, ] <- S0
  for (j in 1:n)
    for (t in 2:(meses + 1))
      M[t, j] <- M[t-1, j] * exp(
        (mu_a - 0.5 * sigma_a^2) * dt + sigma_a * sqrt(dt) * rnorm(1)
      )
  M
}

# ── Precios iniciales (01-may-2026) ──────────────────────────────────────────

ultimos_p <- c(
  NVDA = 198.45,
  TSLA = 390.82,
  COST = 1011.72
)

# Garantizar que el orden coincida con el vector tickers
ultimos_p <- ultimos_p[tickers]


# ── Volatilidades históricas por acción ──────────────────────────────────────
sigma_sim <- as.numeric(sd_anual)
names(sigma_sim) <- tickers

# ── Deriva de cada escenario ─────────────────────────────────────────────────
# Los escenarios se construyen de forma CONSISTENTE entre sí y con los datos
# históricos de mercado, sin depender del nivel de mu_historico de cada acción:
#
#   Alcista  : retorno del S&P 500 en su percentil 75% histórico anual ~ +20%
#              para acciones growth (NVDA/TSLA) se permite hasta +25%
#              para COST (defensiva) se permite hasta +18%
#   Moderado : retorno esperado histórico del S&P 500 anualizado ~ +10%
#              consistente con el largo plazo del índice 2016-2026
#   Bajista  : caída moderada, consistente con correcciones históricas ~ -8%
#              sin llegar a escenario de crisis sistémica
#
# Este enfoque evita que el mu_historico de NVDA (>80% anual 2016-2026)
# distorsione la simulación hacia valores terminales irreales.

param_sim <- data.frame(
  accion    = tickers,
  S0        = as.numeric(ultimos_p),
  sigma     = sigma_sim,
  mu_base   = as.numeric(ret_anual),       # retorno hist. para referencia
  stringsAsFactors = FALSE
) %>%
  mutate(
    # Alcista: mercado favorable, con tope razonable por acción
    mu_alcista  = case_when(
      accion == "NVDA" ~ 0.25,   # 25% anual — alto crecimiento IA
      accion == "TSLA" ~ 0.22,   # 22% anual — expansión EV
      accion == "COST" ~ 0.14    # 14% anual — consumo sólido
    ),
    # Moderado: retorno esperado de largo plazo del S&P 500
    mu_moderado = case_when(
      accion == "NVDA" ~ 0.12,   # 12% — alineado con prima tecnológica
      accion == "TSLA" ~ 0.10,   # 10% — crecimiento normalizado
      accion == "COST" ~ 0.10    # 10% — consumo básico estable
    ),
    # Bajista: corrección de mercado sin crisis sistémica
    mu_bajista  = case_when(
      accion == "NVDA" ~ -0.08,  # -8% — corrección valuación IA
      accion == "TSLA" ~ -0.12,  # -12% — presión márgenes + competencia
      accion == "COST" ~  -0.01   #  -1% — Conservador
    )
  )

kable(
  param_sim %>%
    select(accion, S0, mu_alcista, mu_moderado, mu_bajista, sigma) %>%
    mutate(across(c(mu_alcista,mu_moderado,mu_bajista,sigma),
                  ~paste0(round(.x*100,1),"%")),
           S0 = paste0("$",format(round(S0,2),big.mark=","))),
  caption = "Tabla 17. Parámetros de simulación por escenario y acción.",
  align   = "c",
  col.names = c("Acción","S₀ (precio inicial)","μ Alcista",
                "μ Moderado","μ Bajista","σ Anual")
) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
  row_spec(1, background = "#f0fef4") %>%
  row_spec(2, background = "#fef9f0") %>%
  row_spec(3, background = "#eef4fd")
Tabla 17. Parámetros de simulación por escenario y acción.
Acción S₀ (precio inicial) μ Alcista μ Moderado μ Bajista σ Anual
NVDA NVDA $ 198.45 25% 12% -8% 45.8%
TSLA TSLA $ 390.82 22% 10% -12% 58.5%
COST COST $1,011.72 14% 10% -1% 20.8%
sims <- lapply(tickers, function(tk) {
  f <- param_sim[param_sim$accion == tk, ]
  list(
    alcista  = simular_trayectorias(f$S0, f$mu_alcista,  f$sigma),
    moderado = simular_trayectorias(f$S0, f$mu_moderado, f$sigma),
    bajista  = simular_trayectorias(f$S0, f$mu_bajista,  f$sigma)
  )
})
names(sims) <- tickers
escenarios_ord <- c("alcista","moderado","bajista")

tray_df <- map_dfr(tickers, function(tk) {
  map_dfr(escenarios_ord, function(esc) {
    data.frame(
      mes       = 0:48,
      accion    = tk,
      escenario = esc,
      precio    = rowMeans(sims[[tk]][[esc]]),
      p5        = apply(sims[[tk]][[esc]], 1, quantile, 0.05),
      p95       = apply(sims[[tk]][[esc]], 1, quantile, 0.95)
    )
  })
}) %>%
  mutate(
    escenario = factor(escenario,
                       levels = escenarios_ord,
                       labels = c("Alcista","Moderado","Bajista"))
  )

col_esc <- c("Alcista"="#1a9850","Moderado"="#f4a800","Bajista"="#d73027")

ggplot(tray_df, aes(x = mes, color = escenario, fill = escenario)) +
  geom_ribbon(aes(ymin = p5, ymax = p95), alpha = 0.12, color = NA) +
  geom_line(aes(y = precio), size = 1.0) +
  facet_wrap(~ accion, scales = "free_y", ncol = 1) +
  scale_color_manual(values = col_esc) +
  scale_fill_manual(values  = col_esc) +
  scale_x_continuous(breaks = seq(0, 48, 6),
                     labels = c("Abr-26","Oct-26","Abr-27","Oct-27",
                                "Abr-28","Oct-28","Abr-29","Oct-29","Abr-30")) +
  labs(title    = "Trayectorias de precio promedio por escenario — 48 meses",
       subtitle = "Línea = precio promedio | Banda = percentil 5% – 95%",
       x = NULL, y = "Precio esperado (USD)",
       color = "Escenario", fill = "Escenario",
       caption = "Fuente: Elaboración propia. Modelo MBG (Hull, 2018).") +
  theme_minimal(base_size = 11) +
  theme(plot.title     = element_text(face = "bold"),
        plot.subtitle  = element_text(color = "gray40", size = 10),
        legend.position= "bottom",
        axis.text.x    = element_text(angle = 30, hjust = 1, size = 8))
Figura 9. Trayectorias de precio promedio por acción y escenario — horizonte 48 meses. Elaboración propia.

Figura 9. Trayectorias de precio promedio por acción y escenario — horizonte 48 meses. Elaboración propia.

# Valor del portafolio a 48 meses por escenario
# Cada trayectoria es un vector de 1,000 valores terminales del portafolio
calc_val_port <- function(esc) {
  vals <- lapply(tickers, function(tk) {
    w_i  <- pesos[tk]
    S0_i <- param_sim$S0[param_sim$accion == tk]
    pf   <- sims[[tk]][[esc]][49, ]          # fila 49 = mes 48 = abr-2030
    capital * w_i * (pf / S0_i)             # valor terminal ponderado
  })
  Reduce(`+`, vals)
}

vp_alc <- calc_val_port("alcista")
vp_mod <- calc_val_port("moderado")
vp_baj <- calc_val_port("bajista")

# Verificación de coherencia: mediana alcista > moderado > bajista
# y todos > 0 (no puede haber portafolio con valor negativo en MBG)
stopifnot(
  median(vp_alc) > median(vp_mod),
  median(vp_mod) > median(vp_baj),
  all(vp_baj > 0)
)

fmt_usd <- function(x) paste0("$", format(round(x, 0), big.mark = ","))

res48 <- data.frame(
  Escenario       = c("Alcista","Moderado","Bajista"),
  `Capital inicial` = fmt_usd(capital),
  `Media`         = fmt_usd(c(mean(vp_alc), mean(vp_mod), mean(vp_baj))),
  `Mediana`       = fmt_usd(c(median(vp_alc), median(vp_mod), median(vp_baj))),
  `Perc. 5%`      = fmt_usd(c(quantile(vp_alc,.05), quantile(vp_mod,.05),
                               quantile(vp_baj,.05))),
  `Perc. 95%`     = fmt_usd(c(quantile(vp_alc,.95), quantile(vp_mod,.95),
                               quantile(vp_baj,.95))),
  `Retorno medio` = paste0(round(c(mean(vp_alc/capital)-1,
                                   mean(vp_mod/capital)-1,
                                   mean(vp_baj/capital)-1)*100, 1),"%"),
  check.names = FALSE, row.names = NULL
)

kable(res48,
      caption = "Tabla 18. Valor del portafolio a 48 meses — media, mediana y percentiles.",
      align   = c("l","c","c","c","c","c","c")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#2c3e50", color = "white") %>%
  row_spec(1, background = "#f0fef4") %>%
  row_spec(2, background = "#fef9f0") %>%
  row_spec(3, background = "#fef0f0")
Tabla 18. Valor del portafolio a 48 meses — media, mediana y percentiles.
Escenario Capital inicial Media Mediana Perc. 5% Perc. 95% Retorno medio
Alcista $20,000,000 $47,592,935 $33,848,733 $9,553,539 $126,527,572 138%
Moderado $20,000,000 $29,951,511 $21,618,955 $6,438,577 $ 78,295,996 49.8%
Bajista $20,000,000 $14,313,078 $10,559,911 $3,362,444 $ 36,493,175 -28.4%

La simulación a 48 meses utiliza el modelo de movimiento browniano geométrico para proyectar la evolución de NVDA, TSLA y COST bajo tres escenarios: alcista, moderado y bajista. Los parámetros muestran que NVDA y TSLA tienen mayores retornos esperados en escenarios favorables, pero también mayor exposición en escenarios bajistas, coherente con su perfil de crecimiento y alta volatilidad. Por otrom lado, COST presenta retornos esperados más moderados y una volatilidad anual menor, lo que refleja su papel conservador dentro del portafolio.

La tabla de valor del portafolio se evidencia que este tendría un desempeño significativamente distinto según el escenario. En el escenario alcista, el valor medio proyectado asciende a USD 49.2 millones, con un retorno medio de 146%, mientras que en el escenario moderado alcanza USD 30.9 millones, equivalente a 54.4%. En contraste, el escenario bajista reduce el valor medio a USD 15.1 millones, con un retorno medio de -24.4%. La diferencia entre media, mediana y percentiles muestra que existe alta dispersión en los resultados, especialmente por la volatilidad de NVDA y TSLA; por tanto, la simulación confirma que el portafolio tiene alto potencial de valorización, pero también una exposición importante a pérdidas en escenarios no tan buenos.

dist_df <- data.frame(
  Valor     = c(vp_alc, vp_mod, vp_baj) / 1e6,
  Escenario = rep(c("Alcista","Moderado","Bajista"),
                  each = 1000)
) %>%
  mutate(Escenario = factor(Escenario, levels = c("Alcista","Moderado","Bajista")))

ggplot(dist_df, aes(x = Valor, fill = Escenario, color = Escenario)) +
  geom_density(alpha = 0.25, size = 0.9) +
  geom_vline(data = dist_df %>%
               group_by(Escenario) %>%
               summarise(med = median(Valor), .groups="drop"),
             aes(xintercept = med, color = Escenario),
             linetype = "dashed", size = 1) +
  scale_fill_manual(values  = col_esc) +
  scale_color_manual(values = col_esc) +
  scale_x_continuous(labels = scales::dollar_format(suffix = "M", prefix = "$")) +
  labs(title    = "Distribución del valor terminal del portafolio (mes 48 — abr 2030)",
       subtitle = "Líneas punteadas = mediana por escenario | 1,000 trayectorias simuladas",
       x = "Valor del portafolio (USD millones)",
       y = "Densidad",
       caption = "Fuente: Elaboración propia. Modelo MBG (Hull, 2018).") +
  theme_minimal(base_size = 12) +
  theme(plot.title     = element_text(face = "bold"),
        plot.subtitle  = element_text(color = "gray40", size = 10),
        legend.position= "bottom")
Figura 10. Distribución del valor terminal del portafolio a 48 meses por escenario. Elaboración propia.

Figura 10. Distribución del valor terminal del portafolio a 48 meses por escenario. Elaboración propia.

La mediana resulta más representativa que la media para describir el valor terminal del portafolio, dado que las distribuciones log-normales del modelo MBG son asimétricas hacia la derecha — la media queda inflada por las trayectorias más extremas del lado positivo. Los percentiles 5% y 95% delimitan el rango que contiene el 90% de los resultados simulados y representan los escenarios pesimista y optimista razonables para cada hipótesis de mercado.


13 Sensibilidad de la cobertura ante cambios en beta

betas_hip <- c(0.5, 2.0)

hip_df <- data.frame(
  `Beta hipotética` = betas_hip,
  `Exposición sistemática (USD)` =
    paste0("$",format(round(betas_hip*capital,0),big.mark=",")),
  `N* exacto` = round(betas_hip*capital/(F0*QF),4),
  `N* redondeado` = round(betas_hip*capital/(F0*QF)),
  `Margen inicial (USD)` =
    paste0("$",format(round(round(betas_hip*capital/(F0*QF))*margen_ini,0),
                      big.mark=",")),
  `Sensib. 1% S&P500 (USD)` =
    paste0("$",format(round(betas_hip*0.01*capital,0),big.mark=",")),
  check.names=FALSE, row.names=NULL
)
kable(hip_df,
      caption="Tabla 19. Sensibilidad de la cobertura con beta hipotética 0.5 y 2.",
      align=c("c","c","c","c","c","c")) %>%
  kable_styling(bootstrap_options=c("striped","hover"), full_width=TRUE) %>%
  row_spec(0, bold=TRUE, background="#2c3e50", color="white") %>%
  row_spec(1, background="#eaf4d3") %>%
  row_spec(2, background="#fef0f0")
Tabla 19. Sensibilidad de la cobertura con beta hipotética 0.5 y 2.
Beta hipotética Exposición sistemática (USD) N* exacto N* redondeado Margen inicial (USD) Sensib. 1% S&P500 (USD)
0.5 $10,000,000 27.9018 28 $ 740,516 $100,000
2.0 $40,000,000 111.6071 112 $2,962,064 $400,000

Con beta = 0.5, el portafolio es menos sensible al mercado que un índice pasivo: ante una caída del 10% en el S&P 500, se esperaría una pérdida de solo el 5% en el portafolio. En este caso se necesitan menos contratos de futuros para la cobertura, el margen requerido es menor y el costo de la estrategia disminuye. Este perfil corresponde a carteras defensivas o neutras al mercado.

Con beta = 2, el portafolio amplifica los movimientos del mercado en ambas direcciones: ante una caída del 10% en el índice, la pérdida esperada sería del 20%. Para cubrir esa exposición se necesita aproximadamente el doble de contratos respecto al caso beta = 1, lo que implica un mayor desembolso de margen y una gestión más activa de los llamados al margen cuando el mercado se mueve en sentido contrario a la posición. Portafolios con beta elevada son típicos de carteras concentradas en acciones de crecimiento o con apalancamiento implícito.

La relación es proporcional: aumentar la beta aumenta el número de contratos y la exposición al riesgo. Esto muestra por qué la beta del portafolio es el parámetro central de la estrategia de cobertura con futuros — cambios pequeños en beta tienen efectos directos sobre el costo y la efectividad de la cobertura.


Referencias

Black, F., & Litterman, R. (1992). Global portfolio optimization. Financial Analysts Journal, 48(5), 28–43. https://doi.org/10.2469/faj.v48.n5.28

CME Group. (2026). E-mini S&P 500 margins. Chicago Mercantile Exchange. https://www.cmegroup.com/markets/equities/sp/e-mini-sandp500.margins.html

Costco Wholesale Corporation. (2025). Annual report / Form 10-K. https://investor.costco.com/financials/annual-reports-and-proxy-statements/default.aspx

Costco Wholesale Corporation. (2025). Annual report 2025. https://s201.q4cdn.com/287523651/files/doc_financials/2025/ar/COST-Annual-Report-2025.pdf

Markowitz, H. (1952). Portfolio selection. Journal of Finance, 7(1), 77–91. https://doi.org/10.2307/2975974

NVIDIA Corporation. (2025). Annual report 2025. https://investor.nvidia.com/financial-info/annual-reports-and-proxies/default.aspx

Sharpe, W. F. (1964). Capital asset prices: A theory of market equilibrium under conditions of risk. Journal of Finance, 19(3), 425–442. https://doi.org/10.2307/2977928

Tesla, Inc. (2025). Form 10-K for the fiscal year ended December 31, 2024. U.S. Securities and Exchange Commission. https://ir.tesla.com/_flysystem/s3/sec/000162828025003063/tsla-20241231-gen.pdf

Yahoo Finance. (2026). Datos históricos de precios — NVDA, TSLA, COST, ^GSPC, ES=F [Base de datos]. https://finance.yahoo.com

Yahoo Finance / CBOE. (2026). CBOE Interest Rate 3–5 Year Treasury Index (^TNX). https://finance.yahoo.com/quote/%5ETNX