title: “Cobertura con Futuros sobre Índice Bursátil”

author: Liz Johana Ramírez Castaño · José Manuel Sánchez Loaiza · Yonathan Alvarez

date: ‘Fecha inicial de inversión: 30 de abril de 2026’

always_allow_html: true

output:

html_document:

theme: flatly

highlight: tango

toc: true

toc_float:

  collapsed: False

  smooth_scroll: true

toc_depth: 4

number_sections: true

df_print: paged

code_folding: hide

subtitle: ‘Portafolio: PG · SYK · WM | Capital: USD 20,000,000’



Objetivo del ejercicio: Construir un portafolio accionario óptimo con PG (Procter & Gamble), SYK (Stryker) y WM (Waste Management), pertenecientes al S&P 500, y diseñar una estrategia de cobertura mediante futuros E-mini S&P 500 con horizonte de cuatro años.

Entregable: Informe interactivo HTML publicado en RPubs. Los cálculos y gráficas son reproducibles desde el código adjunto.


1 Introducción

Este informe presenta una estrategia de inversión y cobertura financiera para un capital de USD 20.000.000, con horizonte de 4 años desde el 30 de abril de 2026. El portafolio está compuesto por tres acciones del índice S&P 500, seleccionadas con base en criterios de solidez fundamental, diversificación sectorial y complementariedad de riesgo-retorno:

  • PG — Procter & Gamble (Consumo básico defensivo)

  • SYK — Stryker Corporation (Sector salud / dispositivos médicos)

  • WM — Waste Management (Servicios ambientales)

La cobertura se implementa mediante contratos de futuros E-mini S&P 500 (ES) negociados en el CME Group. El marco teórico se apoya en la teoría de portafolio de Markowitz (1952), el CAPM de Sharpe (1964) y la metodología de cobertura con futuros de Hull (2022).


2 Parámetros Globales

CAPITAL       <- 20e6

FECHA_INI     <- as.Date("2026-04-30")

FECHA_HIST    <- as.Date("2016-04-30")   # 10 años históricos

TICKERS       <- c("PG", "SYK", "WM")

INDICE        <- "^GSPC"

N_ANUAL       <- 252                     # días hábiles

# Tasa libre de riesgo: ^FVX (CBOE 5-Year Treasury Note Index)
# Proxy del rango 3-5 años especificado en el enunciado.
# Fuente: Yahoo Finance getSymbols("^FVX") al 30/04/2026
RF_ANUAL      <- 0.0418                  # ^FVX al 30/04/2026 (~4.18% anual)

HORIZONTE     <- 4                       # años

N_MESES       <- HORIZONTE * 12          # 48 meses

N_MES         <- 21                      # días hábiles por mes

N_SIM         <- 5000                    # simulaciones GBM

# Contrato E-mini S&P 500 (CME) — datos al 30/04/2026

MULTIPLICADOR      <- 50

MARGEN_INI         <- 14000

MARGEN_MANT        <- 12700

precio_futuro_ini  <- 7168   # Precio F₀ CME al 30/04/2026

# Dividendos anuales aproximados (yield publicado por cada empresa)

DIV_PG  <- 0.024   # 2.4% anual

DIV_SYK <- 0.010   # 1.0% anual

DIV_WM  <- 0.015   # 1.5% anual

params_df <- data.frame(

  Parámetro = c("Capital inicial","Fecha inicial","Horizonte","Tickers",

                "Índice","Días hábiles/año","Tasa libre de riesgo (^FVX 5Y)",

                "Multiplicador E-mini","Margen inicial/contrato",

                "Margen mantenimiento/contrato","Precio futuro inicial (F₀)"),

  Valor = c(

    dollar(CAPITAL), as.character(FECHA_INI),

    paste(HORIZONTE,"años"), paste(TICKERS, collapse=" · "),

    "S&P 500 (^GSPC)", N_ANUAL,

    paste0(RF_ANUAL*100,"% anual"),

    paste0("USD ",MULTIPLICADOR," / punto"),

    dollar(MARGEN_INI), dollar(MARGEN_MANT),

    paste(precio_futuro_ini,"puntos")

  )

)

kable(params_df, align=c("l","r"), caption="Tabla 1. Parámetros globales del ejercicio") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(0, background="#003087", color="white")
Tabla 1. Parámetros globales del ejercicio
Parámetro Valor
Capital inicial $20,000,000
Fecha inicial 2026-04-30
Horizonte 4 años
Tickers PG · SYK · WM
Índice S&P 500 (^GSPC)
Días hábiles/año 252
Tasa libre de riesgo (^FVX 5Y) 4.18% anual
Multiplicador E-mini USD 50 / punto
Margen inicial/contrato $14,000
Margen mantenimiento/contrato $12,700
Precio futuro inicial (F₀) 7168 puntos

Tasa libre de riesgo — ^FVX: El CBOE Interest Rate 5-Year Treasury Note Index (proxy del rango 3–5 años indicado en el enunciado) reportó una tasa de 4.18% anual al 30 de abril de 2026, extraída de Yahoo Finance (getSymbols("^FVX")). Se prefiere el vencimiento de 5 años por ser el más cercano al horizonte de inversión de 4 años del ejercicio. Esta tasa se usa como referencia para el Sharpe Ratio y el valor esperado de la cobertura (Hull, 2022).


3 Análisis Fundamental de las Acciones

Criterios de selección: Las tres acciones fueron seleccionadas priorizando (i) diversificación sectorial real dentro del S&P 500, (ii) estabilidad financiera medida por flujo de caja libre positivo y dividendos consistentes, y (iii) sensibilidades distintas al ciclo económico para maximizar los beneficios de la diversificación.

3.1 Procter & Gamble (PG) — Consumo Básico Defensivo

PG The Procter & Gamble Company | NYSE | S&P 500

Descripción general: Procter & Gamble es uno de los mayores fabricantes globales de bienes de consumo masivo. Con presencia en más de 180 países, sus marcas incluyen Tide, Pampers, Gillette, Oral-B y Head & Shoulders. La empresa opera bajo un modelo de negocio centrado en marcas de alta lealtad con poder de fijación de precios.

Sector y fuentes de ingresos: Consumo básico (Consumer Staples). Los ingresos se distribuyen en cinco segmentos: Fabric & Home Care (~35%), Baby, Feminine & Family Care (~25%), Beauty (~19%), Health Care (~13%) y Grooming (~8%). La diversificación geográfica mitiga riesgos de concentración de mercado.

Situación financiera: PG registra márgenes operativos superiores al 20%, flujo de caja libre robusto (FCF ~$14B anuales) y ha incrementado su dividendo por más de 65 años consecutivos, lo que la clasifica como Dividend King. La deuda neta es manejable frente a su EBITDA (~2.0x).

Evolución reciente del precio: Entre 2020 y 2026, PG mostró un crecimiento moderado y sostenido, con caídas controladas durante períodos de estrés de mercado (2022). Su beta inferior a 0.60 confirma su carácter defensivo y su baja correlación con ciclos económicos.

Expectativa de precio a un año: Dado el contexto de normalización de tasas y recuperación del consumo en mercados emergentes, los analistas proyectan un rango de precio entre USD 170–190 para el segundo trimestre de 2027, con un sesgo alcista derivado de la expansión de márgenes post-inflación y la fortaleza de sus marcas premium.

Justificación de inclusión: PG actúa como ancla defensiva del portafolio. Su baja volatilidad (~18.9% anual) y beta reducida (~0.48) amortiguan el riesgo sistémico del conjunto, especialmente relevante en escenarios de recesión o corrección de mercado. Su inclusión complementa activos de mayor crecimiento como SYK.

3.2 Stryker Corporation (SYK) — Dispositivos Médicos

SYK Stryker Corporation | NYSE | S&P 500

Descripción general: Stryker es un líder global en dispositivos médicos y equipamiento para salas de cirugía. Sus líneas de producto incluyen implantes ortopédicos, equipos de neuromonitorización, camas hospitalarias inteligentes y soluciones robóticas (sistema Mako). Opera principalmente en Norteamérica y Europa.

Sector y fuentes de ingresos: Salud (Health Care). Los ingresos se distribuyen en MedSurg & Neurotechnology (~52%) y Orthopaedics & Spine (~48%). El segmento de cirugía robótica asistida (Mako) presenta la tasa de crecimiento más elevada del portafolio de productos.

Situación financiera: SYK registra crecimientos de ingresos del 8-12% anual en los últimos cinco años, impulsados por la recuperación de procedimientos electivos post-COVID y la adopción acelerada de cirugía robótica. El margen operativo ajustado supera el 24%, con FCF creciente. La deuda es moderada (2.5x EBITDA) tras adquisiciones estratégicas.

Evolución reciente del precio: SYK mostró alta volatilidad en 2020 (caída por cancelación de cirugías electivas) y fuerte recuperación en 2021-2023. Entre 2023 y 2026, la acción retomó una tendencia alcista sostenida impulsada por el crecimiento del segmento robótico y la expansión internacional.

Expectativa de precio a un año: El envejecimiento poblacional en mercados desarrollados y la penetración del sistema Mako en nuevos procedimientos sugieren un rango de USD 420–470 para Q2 2027, con catalizadores adicionales en aprobaciones regulatorias de nuevos dispositivos.

Justificación de inclusión: SYK aporta exposición al crecimiento secular en salud. Su mayor volatilidad (~26.1% anual) se compensa con retornos esperados superiores. Es el activo de mayor contribución al rendimiento del portafolio y su correlación moderada con PG y WM (~0.39–0.50) permite diversificación efectiva.

3.3 Waste Management (WM) — Servicios Ambientales

WM Waste Management, Inc. | NYSE | S&P 500

Descripción general: Waste Management es el mayor operador de servicios ambientales en Norteamérica, con operaciones en recolección de residuos, reciclaje, tratamiento y disposición final. Su modelo de negocio se caracteriza por contratos municipales de largo plazo y activos físicos difíciles de replicar (vertederos con permisos exclusivos).

Sector y fuentes de ingresos: Servicios Ambientales (Industrials / Utilities). Los ingresos provienen principalmente de servicios de recolección (~58%), vertederos de disposición (~20%) y transferencia y reciclaje (~22%). Los contratos municipales aportan flujos de caja predecibles y con ajustes por inflación.

Situación financiera: WM registra márgenes EBITDA superiores al 30%, con FCF estable de ~$3.5-4.0B anuales. La visibilidad de flujos es alta dado el carácter esencial del servicio y la estructura regulatoria. Ha incrementado su dividendo por 20+ años consecutivos. La deuda (~3.0x EBITDA) es característica del sector y manejable dado el FCF recurrente.

Evolución reciente del precio: WM mostró el mayor crecimiento relativo entre los tres activos durante el período 2016-2026, alcanzando un rendimiento acumulado cercano al 380% en términos ajustados. La baja correlación con el ciclo industrial y el carácter esencial de su negocio limitaron las caídas en mercados bajistas (2020, 2022).

Expectativa de precio a un año: La expansión de capacidad de reciclaje avanzado (Advanced Recycling), los contratos de gas landfill y la presión regulatoria hacia sostenibilidad apuntan a un rango de USD 240–275 para Q2 2027, con soporte fundamental en crecimiento de volúmenes y poder de fijación de precios.

Justificación de inclusión: WM ofrece la mejor relación riesgo-retorno del universo analizado. Su volatilidad moderada (~19.5% anual) combinada con el mayor retorno esperado del grupo la convierte en el activo dominante bajo el criterio de media-varianza. Su carácter defensivo-esencial la posiciona como motor principal del portafolio.


4 Datos Históricos y Retornos

4.1 Metodología de datos y calidad

Fuente de datos: Yahoo Finance vía paquete quantmod en R (Keitt, 2023). Se utilizan precios ajustados por dividendos y splits (columna Ad()), lo que garantiza que los retornos calculados incorporan el retorno total del accionista (apreciación de capital + dividendos reinvertidos). Este tratamiento es coherente con la práctica estándar en gestión de portafolios (Sharpe, 1964; Hull, 2022).

Período histórico: 10 años: 30 de abril de 2016 al 30 de abril de 2026.

Frecuencia: Diaria. Se eliminan observaciones con valores faltantes mediante na.omit().

Tratamiento de dividendos: Al usar precios ajustados, los dividendos ya están implícitos en los retornos calculados. No se deben sumar nuevamente para evitar doble conteo. En la sección de rendimiento esperado se desglosa el efecto de los dividendos sobre el retorno total de cada acción.

4.2 Descarga y verificación de precios

descargar <- function(tickers, desde, hasta) {

  lst <- lapply(tickers, function(tk) {

    tryCatch({

      raw <- getSymbols(tk, src="yahoo", from=desde, to=hasta,

                        auto.assign=FALSE, warnings=FALSE)

      Ad(raw)

    }, error=function(e) { message("Error: ",tk); NULL })

  })

  names(lst) <- tickers

  lst <- lst[!sapply(lst, is.null)]

  do.call(merge, lst)

}

precios_acc <- descargar(TICKERS, FECHA_HIST, FECHA_INI)

precios_idx <- descargar(INDICE,  FECHA_HIST, FECHA_INI)

colnames(precios_acc) <- TICKERS

colnames(precios_idx) <- "GSPC"

datos <- na.omit(merge(precios_acc, precios_idx))

# Verificación de calidad de datos

cat("══════════════════════════════════════════════════════\n")
## ══════════════════════════════════════════════════════
cat(sprintf("  Observaciones totales   : %d días hábiles\n",   nrow(datos)))
##   Observaciones totales   : 2513 días hábiles
cat(sprintf("  Fecha inicial efectiva  : %s\n", as.character(index(datos)[1])))
##   Fecha inicial efectiva  : 2016-05-02
cat(sprintf("  Fecha final efectiva    : %s\n", as.character(tail(index(datos),1))))
##   Fecha final efectiva    : 2026-04-29
cat(sprintf("  Años cubiertos          : %.1f años\n",

            as.numeric(diff(range(index(datos))))/365.25))
##   Años cubiertos          : 10.0 años
cat(sprintf("  Valores NA eliminados   : 0 (post na.omit)\n"))
##   Valores NA eliminados   : 0 (post na.omit)
cat(sprintf("  Tickers descargados     : %s\n", paste(colnames(datos), collapse=", ")))
##   Tickers descargados     : PG, SYK, WM, GSPC
cat("══════════════════════════════════════════════════════\n")
## ══════════════════════════════════════════════════════
# Precios iniciales y finales

p_ini <- as.numeric(datos[1, TICKERS])

p_fin <- as.numeric(tail(datos[, TICKERS], 1))

cal_df <- data.frame(

  Acción = TICKERS,

  `Precio Inicial (USD)` = round(p_ini, 2),

  `Precio Final (USD)`   = round(p_fin, 2),

  `Retorno Total (%)`    = round((p_fin/p_ini - 1)*100, 2),

  check.names = FALSE

)

kable(cal_df, caption="Tabla 2. Precios ajustados inicio/fin del período de 10 años") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE)
Tabla 2. Precios ajustados inicio/fin del período de 10 años
Acción Precio Inicial (USD) Precio Final (USD) Retorno Total (%)
PG 61.64 146.46 137.60
SYK 98.37 315.13 220.34
WM 49.97 230.31 360.92

4.3 Evolución de precios (base 100)

df_norm <- as.data.frame(datos[, TICKERS])

df_norm <- sweep(df_norm, 2, as.numeric(df_norm[1,]), "/") * 100

df_norm$fecha <- index(datos)

df_long <- pivot_longer(df_norm, -fecha, names_to="Accion", values_to="Precio")

# Añadir anotaciones de eventos clave

eventos <- data.frame(

  fecha = as.Date(c("2020-03-23","2022-01-03","2022-12-30")),

  label = c("Mín. COVID-19","Inicio alza\ntasas Fed","Fin ciclo\nalcista tasas")

)

p1 <- ggplot(df_long, aes(x=fecha, y=Precio, color=Accion)) +

  geom_vline(data=eventos, aes(xintercept=fecha), linetype="dotted",

             color="gray50", alpha=0.7) +

  geom_line(linewidth=0.75) +

  geom_text(data=eventos, aes(x=fecha, y=Inf, label=label),

            vjust=1.3, size=2.8, color="gray40", angle=90, hjust=1,

            inherit.aes=FALSE) +

  scale_color_manual(values=c(PG="#003087", SYK="#E31837", WM="#00703C")) +

  scale_x_date(date_breaks="1 year", date_labels="%Y") +

  geom_hline(yintercept=100, linetype="dashed", color="gray60") +

  labs(title="Figura 1. Evolución de precios históricos (base 100 = abril 2016)",

       subtitle="Precios ajustados por dividendos y splits | Fuente: Yahoo Finance",

       x=NULL, y="Precio normalizado (base 100)", color="Acción",

       caption="Datos históricos: 10 años hasta 30/04/2026") +

  theme_minimal(base_size=12) +

  theme(legend.position="bottom", axis.text.x=element_text(angle=45, hjust=1))

ggplotly(p1) %>% layout(hovermode="x unified")

Interpretación: WM lidera el crecimiento acumulado (≈480 base 100), confirmando el mayor retorno del grupo con volatilidad moderada. SYK muestra alta volatilidad pero con recuperaciones rápidas post-crisis. PG presenta el comportamiento más defensivo, con caídas limitadas durante COVID-19 y la corrección de 2022, validando su rol de activo de refugio en el portafolio.

4.4 Retornos y estadísticas descriptivas anualizadas

ret_d    <- na.omit(diff(log(datos)))

ret_acc  <- ret_d[, TICKERS]

ret_idx  <- ret_d[, "GSPC"]

mu_anual  <- colMeans(ret_acc)  * N_ANUAL

sd_anual  <- apply(ret_acc, 2, sd) * sqrt(N_ANUAL)

cov_anual <- cov(ret_acc) * N_ANUAL

cor_mat   <- cor(ret_acc)

# Estadísticas completas

skew_v <- sapply(TICKERS, function(tk) {

  r <- as.numeric(ret_acc[,tk])

  mean((r - mean(r))^3) / sd(r)^3

})

kurt_v <- sapply(TICKERS, function(tk) {

  r <- as.numeric(ret_acc[,tk])

  mean((r - mean(r))^4) / sd(r)^4 - 3

})

est_df <- data.frame(

  Acción              = TICKERS,

  `μ diario (%)`      = round(colMeans(ret_acc)*100, 4),

  `μ anual (%)`       = round(mu_anual*100, 2),

  `σ diaria (%)`      = round(apply(ret_acc,2,sd)*100, 4),

  `σ anual (%)`       = round(sd_anual*100, 2),

  `Sesgo`             = round(skew_v, 3),

  `Curtosis exceso`   = round(kurt_v, 3),

  check.names         = FALSE

)

kable(est_df, align="c", digits=4,

      caption="Tabla 3. Estadísticas de retornos diarios y anualizados (N=252)") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  column_spec(3, bold=TRUE, color="darkblue") %>%

  column_spec(5, bold=TRUE, color="darkred") %>%

  row_spec(0, background="#003087", color="white")
Tabla 3. Estadísticas de retornos diarios y anualizados (N=252)
Acción μ diario (%) μ anual (%) σ diaria (%) σ anual (%) Sesgo Curtosis exceso
PG PG 0.0345 8.68 1.1909 18.90 -0.079 10.678
SYK SYK 0.0463 11.68 1.6470 26.15 -0.311 11.223
WM WM 0.0608 15.33 1.2260 19.46 -0.556 12.263

Anualización: Los retornos diarios se anualizan multiplicando la media por 252 días hábiles y la desviación estándar por √252, procedimiento estándar en gestión de portafolios (Bodie et al., 2021). El sesgo negativo en los tres activos y la curtosis positiva confirman colas pesadas en las distribuciones, lo que motiva el uso adicional del VaR histórico y Monte Carlo como complementos al VaR paramétrico.

4.5 Matrices de covarianzas y correlaciones

kable(round(cov_anual*1e4, 4),

      caption="Tabla 4. Matriz de covarianzas anualizadas (×10⁴)") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE)
Tabla 4. Matriz de covarianzas anualizadas (×10⁴)
PG SYK WM
PG 357.3678 192.6061 183.2491
SYK 192.6061 683.5974 239.2148
WM 183.2491 239.2148 378.7504
kable(round(cor_mat, 4),

      caption="Tabla 5. Matriz de correlaciones entre retornos diarios") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE)
Tabla 5. Matriz de correlaciones entre retornos diarios
PG SYK WM
PG 1.0000 0.3897 0.4981
SYK 0.3897 1.0000 0.4701
WM 0.4981 0.4701 1.0000
corrplot(cor_mat, method="color", type="upper", tl.col="black",

         tl.srt=45, addCoef.col="black", number.cex=1.2,

         col=colorRampPalette(c("#E31837","white","#003087"))(200),

         mar=c(0,0,2,0), title="Figura 2. Correlaciones entre retornos diarios")

Interpretación: Las correlaciones son positivas pero moderadas (0.39–0.50). Ningún par supera 0.50, lo que garantiza beneficios reales de diversificación. La correlación más baja (PG-SYK = 0.39) es la combinación más valiosa para reducir riesgo, dado que los ciclos de consumo básico y dispositivos médicos responden de manera diferencial a los mismos shocks macroeconómicos. La covarianza más alta de SYK (diagonal) confirma que es el activo de mayor riesgo propio.


5 Optimización Media-Varianza

5.1 Marco teórico

Problema de optimización (Markowitz, 1952):

\[\min_w \; w^\top \Sigma w \quad \text{s.a.} \quad w^\top \mu = \mu^* \;, \quad \mathbf{1}^\top w = 1 \;, \quad w_i \geq 0\]

Sharpe Ratio (Sharpe, 1964):

\[SR = \frac{\mu_p - r_f}{\sigma_p}\]

5.2 Frontera eficiente

n <- length(TICKERS)

mu_seq   <- seq(min(mu_anual), max(mu_anual), length.out=300)

frontera <- data.frame(mu=numeric(0), sigma=numeric(0))

for (mu_t in mu_seq) {

  tryCatch({

    Dmat <- 2*cov_anual; dvec <- rep(0,n)

    Amat <- cbind(rep(1,n), mu_anual, diag(n))

    bvec <- c(1, mu_t, rep(0,n))

    sol  <- solve.QP(Dmat, dvec, Amat, bvec, meq=2)

    w    <- sol$solution

    if (all(w >= -1e-8) && abs(sum(w)-1) < 1e-6) {

      sp <- sqrt(as.numeric(t(w) %*% cov_anual %*% w))

      frontera <- rbind(frontera, data.frame(mu=mu_t, sigma=sp))

    }

  }, error=function(e) NULL)

}

# Portafolio máximo Sharpe (tangencia)

sharpe_v <- (frontera$mu - RF_ANUAL) / frontera$sigma

idx_opt  <- which.max(sharpe_v)

mu_opt   <- frontera$mu[idx_opt]

Dmat <- 2*cov_anual; dvec <- rep(0,n)

Amat <- cbind(rep(1,n), mu_anual, diag(n))

bvec <- c(1, mu_opt, rep(0,n))

sol_opt    <- solve.QP(Dmat, dvec, Amat, bvec, meq=2)

w_opt      <- sol_opt$solution / sum(sol_opt$solution)

names(w_opt) <- TICKERS

mu_port    <- as.numeric(t(w_opt) %*% mu_anual)

sigma_port <- as.numeric(sqrt(t(w_opt) %*% cov_anual %*% w_opt))

sharpe_opt <- (mu_port - RF_ANUAL) / sigma_port

monto_opt  <- w_opt * CAPITAL

# Mínima varianza

idx_mv   <- which.min(frontera$sigma)

mu_mv    <- frontera$mu[idx_mv]

sigma_mv <- frontera$sigma[idx_mv]

cat(sprintf("✔ Retorno portafolio óptimo : %.2f%% anual\n", mu_port*100))
## ✔ Retorno portafolio óptimo : 15.22% anual
cat(sprintf("✔ Volatilidad portafolio    : %.2f%% anual\n", sigma_port*100))
## ✔ Volatilidad portafolio    : 19.26% anual
cat(sprintf("✔ Sharpe Ratio              : %.4f\n", sharpe_opt))
## ✔ Sharpe Ratio              : 0.5732

5.3 Gráfico de la frontera eficiente

pts_acc <- data.frame(

  ticker = TICKERS,

  sigma  = sqrt(diag(cov_anual))*100,

  mu     = mu_anual*100

)

cml_x  <- seq(0, max(frontera$sigma)*1.15, length.out=100)

cml_y  <- RF_ANUAL + sharpe_opt * cml_x

cml_df <- data.frame(sigma=cml_x*100, mu=cml_y*100)

p2 <- ggplot() +

  geom_line(data=cml_df, aes(x=sigma, y=mu),

            color="darkorange", linetype="dashed", linewidth=1.1) +

  geom_path(data=data.frame(sigma=frontera$sigma*100, mu=frontera$mu*100),

            aes(x=sigma, y=mu), color="steelblue", linewidth=1.8) +

  geom_point(data=pts_acc, aes(x=sigma, y=mu, color=ticker), size=5, shape=17) +

  geom_label_repel(data=pts_acc, aes(x=sigma, y=mu, label=ticker, color=ticker),

                   fontface="bold", size=4, show.legend=FALSE, nudge_y=0.4) +

  geom_point(aes(x=sigma_mv*100, y=mu_mv*100), color="purple", size=4, shape=15) +

  annotate("text", x=sigma_mv*100+0.3, y=mu_mv*100-0.4,

           label="Mín. Varianza", color="purple", size=3.2, hjust=0) +

  geom_point(aes(x=sigma_port*100, y=mu_port*100), color="red", size=7, shape=18) +

  annotate("label", x=sigma_port*100+0.3, y=mu_port*100+0.5,

           label=sprintf("Portafolio Óptimo\nSharpe=%.3f", sharpe_opt),

           color="red", size=3.2, hjust=0, fill="white", label.size=0.3) +

  geom_point(aes(x=0, y=RF_ANUAL*100), color="black", size=3) +

  annotate("text", x=0.2, y=RF_ANUAL*100+0.2,

           label=sprintf("rf = %.2f%%", RF_ANUAL*100), size=3, hjust=0) +

  scale_color_manual(values=c(PG="#003087", SYK="#E31837", WM="#00703C")) +

  scale_x_continuous(labels=function(x) paste0(x,"%"), limits=c(0, NA)) +

  scale_y_continuous(labels=function(x) paste0(x,"%")) +

  labs(

    title    = "Figura 3. Frontera Eficiente — PG, SYK, WM (S&P 500)",

    subtitle = sprintf("Portafolio óptimo: mu=%.2f%% | sigma=%.2f%% | Sharpe=%.4f | rf=%.2f%%",

                       mu_port*100, sigma_port*100, sharpe_opt, RF_ANUAL*100),

    x="Riesgo — Desviación estándar anual (%)", y="Retorno esperado anual (%)",

    color="Acción",

    caption="Línea naranja: Capital Market Line | ◆ Portafolio tangente | ■ Mínima varianza"

  ) +

  theme_minimal(base_size=13) +

  theme(legend.position="bottom",

        plot.title=element_text(face="bold", color="#003087"))

ggplotly(p2)

5.4 Pesos óptimos y asignación de capital

pesos_df <- data.frame(

  Acción          = TICKERS,

  `Peso (%)`      = round(w_opt*100, 2),

  `Monto (USD)`   = dollar(round(monto_opt)),

  `μ anual (%)`   = round(mu_anual*100, 2),

  `σ anual (%)`   = round(sd_anual*100, 2),

  `Div. Yield (%)` = c(DIV_PG*100, DIV_SYK*100, DIV_WM*100),

  check.names     = FALSE

)

kable(pesos_df, align=c("l","c","r","c","c","c"),

      caption="Tabla 6. Pesos óptimos y asignación de capital (USD 20,000,000)") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(which.max(w_opt), bold=TRUE, background="#eef4fb") %>%

  row_spec(0, background="#003087", color="white")
Tabla 6. Pesos óptimos y asignación de capital (USD 20,000,000)
Acción Peso (%) Monto (USD) μ anual (%) σ anual (%) Div. Yield (%)
PG PG 0.00 $0 8.68 18.90 2.4
SYK SYK 3.05 $609,089 11.68 26.15 1.0
WM WM 96.95 $19,390,911 15.33 19.46 1.5
cat(sprintf("\nRetorno esperado portafolio : %.4f%% anual\n", mu_port*100))
## 
## Retorno esperado portafolio : 15.2181% anual
cat(sprintf("Desviación estándar         : %.4f%% anual\n", sigma_port*100))
## Desviación estándar         : 19.2560% anual
cat(sprintf("Sharpe Ratio                : %.4f\n", sharpe_opt))
## Sharpe Ratio                : 0.5732

Nota sobre la concentración del portafolio: El modelo de media-varianza sin restricciones tiende a concentrar el capital en el activo de mayor eficiencia histórica. En este caso, WM (~97%) domina porque su retorno/riesgo histórico supera al de PG y SYK bajo la métrica de Sharpe. En la práctica, un gestor impondría restricciones adicionales (p.ej. \(w_i \leq 60\%\)) para cumplir normas de diversificación. Para efectos académicos, se respeta el resultado matemático del optimizador y se documenta explícitamente esta limitación metodológica. La beta del portafolio resultante (~0.57) confirma un perfil defensivo coherente con la alta participación de WM.


6 VaR Mensual del Portafolio

6.1 Metodología y cálculo

El VaR como base de cobertura: El Valor en Riesgo mensual cuantifica la pérdida máxima esperada en un mes ordinario con un nivel de confianza dado. Este cálculo es el punto de partida para dimensionar la cobertura: si el VaR al 99% es X%, significa que existe un 1% de probabilidad de perder más de X% del capital en un mes. El número de contratos de futuros necesarios debe ser suficiente para compensar estas pérdidas potenciales (Hull, 2022).

# ── Paramétrico (distribución normal) ─────────────────────────

mu_m      <- mu_port / 12

sig_m     <- sigma_port / sqrt(12)

VaR_99_par <- -(mu_m + qnorm(0.01)*sig_m)

VaR_95_par <- -(mu_m + qnorm(0.05)*sig_m)

# ── Histórico (simulación histórica) ──────────────────────────

ret_ph  <- as.numeric(ret_acc %*% w_opt)

n_bl    <- floor(length(ret_ph)/N_MES)

ret_pm  <- sapply(1:n_bl, function(i) sum(ret_ph[((i-1)*N_MES+1):(i*N_MES)]))

VaR_99_hist <- -quantile(ret_pm, 0.01)

VaR_95_hist <- -quantile(ret_pm, 0.05)

# ── Monte Carlo GBM ────────────────────────────────────────────

S0        <- as.numeric(tail(datos[, TICKERS], 1))

names(S0) <- TICKERS

mu_d      <- colMeans(ret_acc)

sd_d      <- apply(ret_acc, 2, sd)

simular_gbm <- function(s0, mu_d, sigma_d, n_sim, n_pasos) {

  paths <- matrix(NA, nrow=n_sim, ncol=n_pasos+1)

  paths[,1] <- s0

  for (t in 2:(n_pasos+1)) {

    Z <- rnorm(n_sim)

    paths[,t] <- paths[,t-1] * exp((mu_d - 0.5*sigma_d^2) + sigma_d*Z)

  }

  paths

}

sim_precios <- lapply(TICKERS, function(tk)

  simular_gbm(S0[tk], mu_d[tk], sd_d[tk], N_SIM, N_MES))

names(sim_precios) <- TICKERS

ret_sim_port <- vapply(seq_len(N_SIM), function(i) {

  r <- sapply(TICKERS, function(tk)

    log(sim_precios[[tk]][i, N_MES+1] / sim_precios[[tk]][i, 1]))

  sum(w_opt * r)

}, numeric(1))

VaR_99_mc <- -quantile(ret_sim_port, 0.01)

VaR_95_mc <- -quantile(ret_sim_port, 0.05)

var_df <- data.frame(

  Método       = c("Paramétrico (normal)","Histórico","Monte Carlo GBM"),

  `VaR 99% (%)` = round(c(VaR_99_par, VaR_99_hist, VaR_99_mc)*100, 3),

  `VaR 99% (USD)` = dollar(round(c(VaR_99_par, VaR_99_hist, VaR_99_mc)*CAPITAL)),

  `VaR 95% (%)` = round(c(VaR_95_par, VaR_95_hist, VaR_95_mc)*100, 3),

  `VaR 95% (USD)` = dollar(round(c(VaR_95_par, VaR_95_hist, VaR_95_mc)*CAPITAL)),

  check.names  = FALSE

)

kable(var_df, align=c("l","c","r","c","r"),

      caption="Tabla 7. VaR mensual del portafolio — tres métodos") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(3, bold=TRUE, background="#eef4fb") %>%

  row_spec(0, background="#003087", color="white")
Tabla 7. VaR mensual del portafolio — tres métodos
Método VaR 99% (%) VaR 99% (USD) VaR 95% (%) VaR 95% (USD)
Paramétrico (normal) 11.663 $2,332,671 7.875 $1,575,023
Histórico 10.704 $2,140,802 6.383 $1,276,533
Monte Carlo GBM 11.966 $2,393,162 7.981 $1,596,208
p_var <- ggplot(data.frame(ret=ret_sim_port*100), aes(x=ret)) +

  geom_histogram(aes(y=after_stat(density)), bins=80,

                 fill="steelblue", alpha=0.65, color="white") +

  geom_density(color="navy", linewidth=1.1) +

  geom_vline(xintercept=-VaR_99_mc*100, color="red",    linetype="dashed", linewidth=1.2) +

  geom_vline(xintercept=-VaR_95_mc*100, color="orange", linetype="dashed", linewidth=1.2) +

  annotate("label", x=-VaR_99_mc*100, y=Inf,

           label=sprintf("VaR 99%%\n–%.2f%%\nUSD %s", VaR_99_mc*100, dollar(round(VaR_99_mc*CAPITAL))),

           color="red", vjust=1.3, size=3, fill="white", label.size=0.3) +

  annotate("label", x=-VaR_95_mc*100, y=Inf,

           label=sprintf("VaR 95%%\n–%.2f%%\nUSD %s", VaR_95_mc*100, dollar(round(VaR_95_mc*CAPITAL))),

           color="darkorange", vjust=1.3, size=3, fill="white", label.size=0.3) +

  labs(title="Figura 4. Distribución de retornos mensuales del portafolio (Monte Carlo GBM)",

       subtitle=sprintf("%s simulaciones | Horizonte: 21 días hábiles",

                        prettyNum(N_SIM, big.mark=",", scientific=FALSE)),

       x="Retorno mensual (%)", y="Densidad") +

  theme_minimal(base_size=12) +

  theme(plot.title=element_text(face="bold"))

ggplotly(p_var)

Vinculación VaR-Cobertura: El VaR al 99% bajo Monte Carlo GBM asciende a $2,393,162 mensuales. Este valor representa el monto que el portafolio podría perder en el peor 1% de los meses simulados. La cobertura con futuros apunta a compensar exactamente estas pérdidas sistémicas: cuando el índice cae y el portafolio pierde valor, la posición corta en futuros genera ganancias que mitigan esa pérdida. El número de contratos óptimos se calibra con la beta del portafolio para que la cobertura sea proporcional a la exposición sistemática que genera estas pérdidas extremas.


7 Betas CAPM

7.1 Estimación mediante regresión OLS

Modelo CAPM (Sharpe, 1964):

\[r_i - r_f = \alpha_i + \beta_i (r_m - r_f) + \varepsilon_i\]

\[\beta_i = \frac{\text{Cov}(r_i, r_m)}{\text{Var}(r_m)} \qquad \beta_p = \sum_i w_i \beta_i\]

betas <- sapply(TICKERS, function(tk)

  cov(as.numeric(ret_acc[,tk]), as.numeric(ret_idx)) / var(as.numeric(ret_idx)))

beta_port <- sum(w_opt * betas)

reg_df <- data.frame(

  Acción  = TICKERS,

  Alpha   = sapply(TICKERS, function(tk)

    round(coef(lm(as.numeric(ret_acc[,tk]) ~ as.numeric(ret_idx)))[1]*N_ANUAL, 5)),

  Beta    = round(betas, 4),

  ``    = sapply(TICKERS, function(tk)

    round(summary(lm(as.numeric(ret_acc[,tk]) ~ as.numeric(ret_idx)))$r.squared, 4)),

  `p-valor β` = sapply(TICKERS, function(tk) {

    m <- summary(lm(as.numeric(ret_acc[,tk]) ~ as.numeric(ret_idx)))

    round(m$coefficients[2,4], 6)

  }),

  `Peso (%)` = round(w_opt*100, 2),

  check.names = FALSE

)

kable(reg_df, align="c",

      caption="Tabla 8. Betas CAPM — regresión OLS sobre S&P 500 (10 años diarios)") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  add_footnote(sprintf("Beta del portafolio ponderado: beta_p = sum(w_i x beta_i) = %.4f", beta_port),

               notation="symbol") %>%

  row_spec(0, background="#003087", color="white")
Tabla 8. Betas CAPM — regresión OLS sobre S&P 500 (10 años diarios)
Acción Alpha Beta p-valor β Peso (%)
PG.(Intercept) PG 0.02688 0.4849 0.2159 0 0.00
SYK.(Intercept) SYK -0.00312 0.9701 0.4518 0 3.05
WM.(Intercept) WM 0.08367 0.5633 0.2749 0 96.95
* Beta del portafolio ponderado: beta_p = sum(w_i x beta_i) = 0.5757
par(mfrow=c(1,3), mar=c(4,4,3,1))

colores_b <- c(PG="#003087", SYK="#E31837", WM="#00703C")

for (tk in TICKERS) {

  x <- as.numeric(ret_idx)*100

  y <- as.numeric(ret_acc[,tk])*100

  m <- lm(y~x)

  plot(x, y, pch=16, cex=0.3, col=adjustcolor(colores_b[tk],0.3),

       xlab="Retorno S&P 500 (%)", ylab=paste("Retorno",tk,"(%)"),

       main=sprintf("%s | beta=%.3f | R2=%.3f", tk, betas[tk],

                    summary(m)$r.squared), cex.main=0.95)

  abline(m, col=colores_b[tk], lwd=2)

  abline(h=0, v=0, col="gray60", lty=2)

}

par(mfrow=c(1,1))

Beta del portafolio ponderado: \(\beta_p = \sum_i w_i \beta_i =\) 0.5757

Con β_p = 0.5757 < 1, el portafolio es más defensivo que el mercado: ante una caída del 10% en el S&P 500, el portafolio se esperaría que cayera aproximadamente 5.76%, reflejando principalmente el comportamiento de WM (peso ~97%, β≈0.56). Esta baja beta es fundamental para el diseño de la cobertura, ya que reduce el número de contratos necesarios comparado con un portafolio agresivo.


8 Instrumento de Cobertura — E-mini S&P 500

F0 <- precio_futuro_ini

QF <- MULTIPLICADOR

VP <- CAPITAL

contrato_df <- data.frame(

  Característica = c("Activo subyacente","Bolsa / Plataforma","Símbolo CME",

                     "Multiplicador","Precio inicial F₀ (30/04/2026)",

                     "Valor nocional por contrato",

                     "Vencimientos disponibles","Margen inicial por contrato",

                     "Margen de mantenimiento por contrato",

                     "Mecanismo de liquidación","Ajuste mark-to-market",

                     "Horas de negociación"),

  Valor = c(

    "Índice S&P 500","CME Group (Chicago Mercantile Exchange)","ES",

    paste0("USD ", QF, " por punto índice"),

    paste0(F0, " puntos"),

    dollar(F0 * QF),

    "Trimestral: Mar (H) / Jun (M) / Sep (U) / Dic (Z)",

    dollar(MARGEN_INI), dollar(MARGEN_MANT),

    "Entrega en efectivo (cash settlement)",

    "Diario en práctica; mensual en este ejercicio (académico)",

    "Domingo 17:00 – Viernes 16:00 (hora CT)"

  )

)

kable(contrato_df, align=c("l","r"),

      caption="Tabla 9. Parámetros del contrato E-mini S&P 500 — CME Group") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(0, background="#003087", color="white")
Tabla 9. Parámetros del contrato E-mini S&P 500 — CME Group
Característica Valor
Activo subyacente Índice S&P 500
Bolsa / Plataforma CME Group (Chicago Mercantile Exchange)
Símbolo CME ES
Multiplicador USD 50 por punto índice
Precio inicial F₀ (30/04/2026) 7168 puntos
Valor nocional por contrato $358,400
Vencimientos disponibles Trimestral: Mar (H) / Jun (M) / Sep (U) / Dic (Z)
Margen inicial por contrato $14,000
Margen de mantenimiento por contrato $12,700
Mecanismo de liquidación Entrega en efectivo (cash settlement)
Ajuste mark-to-market Diario en práctica; mensual en este ejercicio (académico)
Horas de negociación Domingo 17:00 – Viernes 16:00 (hora CT)

9 Número Óptimo de Contratos

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

Donde: \(\beta_p\) = beta del portafolio, \(V_p\) = valor del portafolio (USD 20M), \(F_0\) = precio inicial del futuro (puntos), \(Q_F\) = multiplicador del contrato (USD 50/punto)

N_star_exacto <- beta_port * VP / (F0 * QF)

N_star_round  <- round(N_star_exacto)

error_pct     <- abs(N_star_exacto - N_star_round)/N_star_exacto*100

cat(sprintf("beta_portafolio              = %.4f\n", beta_port))
## beta_portafolio              = 0.5757
cat(sprintf("V_p (capital)                = USD %s\n",    prettyNum(VP, big.mark=",", scientific=FALSE)))
## V_p (capital)                = USD 20,000,000
cat(sprintf("F₀ (precio futuro)           = %d puntos\n", F0))
## F₀ (precio futuro)           = 7168 puntos
cat(sprintf("Q_F (multiplicador)          = USD %d/punto\n", QF))
## Q_F (multiplicador)          = USD 50/punto
cat(sprintf("F₀ × Q_F (nocional/contrato) = USD %s\n",    prettyNum(F0*QF, big.mark=",", scientific=FALSE)))
## F₀ × Q_F (nocional/contrato) = USD 358,400
cat(sprintf("─────────────────────────────────────────────\n"))
## ─────────────────────────────────────────────
cat(sprintf("N* exacto                    = %.4f contratos\n", N_star_exacto))
## N* exacto                    = 32.1242 contratos
cat(sprintf("N* redondeado                = %d contratos\n",   N_star_round))
## N* redondeado                = 32 contratos
cat(sprintf("Error de discretización      = %.2f%%\n",          error_pct))
## Error de discretización      = 0.39%
cat(sprintf("Nocional cubierto total      = USD %s\n",    prettyNum(N_star_round*F0*QF, big.mark=",", scientific=FALSE)))
## Nocional cubierto total      = USD 11,468,800
cat(sprintf("Margen inicial total         = USD %s\n",    prettyNum(N_star_round*MARGEN_INI, big.mark=",", scientific=FALSE)))
## Margen inicial total         = USD 448,000
cat(sprintf("Margen mantenimiento total   = USD %s\n",    prettyNum(N_star_round*MARGEN_MANT, big.mark=",", scientific=FALSE)))
## Margen mantenimiento total   = USD 406,400

Justificación del redondeo: Se aplica round() (entero más cercano) para minimizar el error de cobertura absoluto. Redondear hacia arriba (ceiling) generaría over-hedging, dejando al portafolio con una ligera posición neta corta que puede revertir el riesgo si el mercado sube. Redondear hacia abajo (floor) generaría under-hedging, dejando exposición residual sin cubrir. El error de discretización es de 0.39%, considerado negligible para propósitos prácticos (Hull, 2022).


10 Posición en Futuros: Corta vs Larga

N_CONT <- N_star_round

pos_df <- data.frame(

  Aspecto = c(

    "Posición del portafolio","Riesgo que se desea cubrir",

    "Posición adoptada en futuros",

    "Si el mercado BAJA (escenario favorable a la cobertura)",

    "Si el mercado SUBE (costo de la cobertura)",

    "Cuándo se usaría posición LARGA en futuros",

    "Riesgo residual no cubierto"

  ),

  Descripción = c(

    "LARGA: portafolio de USD 20M en PG, SYK, WM (acciones en cartera)",

    "Caída sistemática del índice S&P 500 → pérdida proporcional al β_p en el valor de las acciones",

    paste0("CORTA en ", N_CONT, " contratos E-mini S&P 500 (posición vendedora de futuros)"),

    paste0("Ganancia en posición corta de futuros compensa la pérdida en acciones ✔ ",

           "(efecto neto: portafolio prácticamente neutral al movimiento del índice)"),

    paste0("Pérdida en posición corta reduce la ganancia en acciones ✗ ",

           "(es el costo explícito de la cobertura; el inversor renuncia al upside del mercado)"),

    "Cuando se anticipa COMPRA futura de acciones y se teme ALZA de precios antes de adquirirlas (cobertura anticipatoria)",

    "Riesgo idiosincrático (α_i) de cada acción; riesgo de base; riesgo de liquidez; cobertura imperfecta por redondeo"

  )

)

kable(pos_df, col.names=c("Aspecto","Descripción"),

      caption="Tabla 10. Análisis de la posición en futuros — justificación técnica") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(3, bold=TRUE, background="#fff3cd") %>%

  row_spec(4, background="#e8f5e9") %>%

  row_spec(5, background="#ffebee") %>%

  row_spec(0, background="#003087", color="white")
Tabla 10. Análisis de la posición en futuros — justificación técnica
Aspecto Descripción
Posición del portafolio LARGA: portafolio de USD 20M en PG, SYK, WM (acciones en cartera)
Riesgo que se desea cubrir Caída sistemática del índice S&P 500 → pérdida proporcional al β_p en el valor de las acciones
Posición adoptada en futuros CORTA en 32 contratos E-mini S&P 500 (posición vendedora de futuros)
Si el mercado BAJA (escenario favorable a la cobertura) Ganancia en posición corta de futuros compensa la pérdida en acciones ✔ (efecto neto: portafolio prácticamente neutral al movimiento del índice)
Si el mercado SUBE (costo de la cobertura) Pérdida en posición corta reduce la ganancia en acciones ✗ (es el costo explícito de la cobertura; el inversor renuncia al upside del mercado)
Cuándo se usaría posición LARGA en futuros Cuando se anticipa COMPRA futura de acciones y se teme ALZA de precios antes de adquirirlas (cobertura anticipatoria)
Riesgo residual no cubierto Riesgo idiosincrático (α_i) de cada acción; riesgo de base; riesgo de liquidez; cobertura imperfecta por redondeo

Análisis posición siempre-corta vs siempre-larga:

  • Siempre corta (nuestro caso): Protege contra caídas del mercado. En mercados alcistas, genera pérdidas en futuros que reducen la rentabilidad total del portafolio. Apropiado para inversores que priorizan la preservación del capital.

  • Siempre larga: Amplifica ganancias en mercados alcistas pero también amplifica pérdidas en bajistas. Solo tiene sentido si se anticipa una compra futura de acciones (eliminar riesgo de precio antes de ejecutar la compra).

Conclusión: Un inversionista que ya posee el portafolio y desea protegerse frente a una caída del mercado durante 4 años debe tomar posición CORTA. La estrategia no elimina el riesgo idiosincrático (riesgo propio de PG, SYK y WM), solo el riesgo sistemático medido por la beta.


11 Flujos Mensuales — Mark-to-Market

11.1 Simulación del precio del índice

mu_idx_d  <- as.numeric(mean(ret_idx))

sd_idx_d  <- as.numeric(sd(ret_idx))

S0_idx    <- as.numeric(tail(datos[,"GSPC"], 1))

set.seed(99)

precio_idx_mes <- numeric(N_MESES + 1)

precio_idx_mes[1] <- S0_idx

for (m in 2:(N_MESES+1)) {

  Z <- rnorm(N_MES)

  precio_idx_mes[m] <- precio_idx_mes[m-1] *

    exp(sum((mu_idx_d - 0.5*sd_idx_d^2) + sd_idx_d*Z))

}

T_trim <- 0.25

precio_fut_mes <- precio_idx_mes * exp(RF_ANUAL * T_trim)

# Mark-to-market mensual con margin call

saldo_margen <- MARGEN_INI * N_CONT

mtm_lst <- vector("list", N_MESES)

for (m in 1:N_MESES) {

  F_ini <- precio_fut_mes[m]

  F_fin <- precio_fut_mes[m+1]

  dF    <- F_fin - F_ini

  GP_l  <-  dF * QF * N_CONT   # posición larga

  GP_c  <- -GP_l                # posición corta

  saldo_margen <- saldo_margen + GP_c

  mc <- 0

  if (saldo_margen < MARGEN_MANT * N_CONT) {

    mc <- MARGEN_INI * N_CONT - saldo_margen

    saldo_margen <- MARGEN_INI * N_CONT

  }

  mtm_lst[[m]] <- data.frame(

    Mes=m, Trim=ceiling(m/3),

    F_ini=round(F_ini,2), F_fin=round(F_fin,2), dF=round(dF,2),

    GP_larga=round(GP_l), GP_corta=round(GP_c),

    Saldo_margen=round(saldo_margen), Margin_call=round(mc)

  )

}

mtm_tabla <- do.call(rbind, mtm_lst)

# ── Tabla interactiva COMPLETA: 48 meses ─────────────────────

mtm_tabla$Anio <- ceiling(mtm_tabla$Mes / 12)

datatable(
  mtm_tabla[, c("Anio","Trim","Mes","F_ini","F_fin","dF",
                "GP_larga","GP_corta","Saldo_margen","Margin_call")],
  colnames = c("Año","Trim.","Mes","F Inicial","F Final","ΔF",
               "G/P Larga (USD)","G/P Corta (USD)",
               "Saldo Margen (USD)","Margin Call (USD)"),
  caption  = paste0("Tabla 11. Flujos mensuales — 48 meses completos | ",
                    N_CONT, " contratos E-mini S&P 500 (posición corta)"),
  options  = list(
    pageLength = 12, scrollX = TRUE, dom = "lfrtip",
    columnDefs = list(list(className="dt-center", targets=c(0,1,2))),
    language = list(
      search     = "Buscar:",
      lengthMenu = "Mostrar _MENU_ filas",
      info       = "Mostrando _START_ a _END_ de _TOTAL_ meses",
      paginate   = list(previous="Anterior", `next`="Siguiente")
    )
  ),
  rownames = FALSE, filter = "top"
) %>%
  formatRound(columns=c("F_ini","F_fin","dF"), digits=2) %>%
  formatCurrency(columns=c("GP_larga","GP_corta","Saldo_margen","Margin_call"),
                 currency="$", digits=0) %>%
  formatStyle("Margin_call",
              backgroundColor = styleInterval(0, c("white","#fff3cd")),
              fontWeight      = styleInterval(0, c("normal","bold"))) %>%
  formatStyle("GP_corta",
              color = styleInterval(0, c("#c62828","#2e7d32"))) %>%
  formatStyle("Saldo_margen",
              backgroundColor = styleInterval(MARGEN_MANT*N_CONT, c("#ffebee","white")))
# ── Resumen trimestral ────────────────────────────────────────

resumen_mtm <- mtm_tabla %>%
  group_by(Trim) %>%
  summarise(
    `GP Corta (USD)`    = sum(GP_corta),
    `Margin Calls`      = sum(Margin_call > 0),
    `Reponer (USD)`     = sum(Margin_call),
    `Saldo Final (USD)` = last(Saldo_margen),
    .groups = "drop"
  ) %>%
  mutate(`GP Acumulada (USD)` = cumsum(`GP Corta (USD)`),
         Resultado = ifelse(`GP Corta (USD)` >= 0, "Ganancia ✔", "Pérdida ✗"))

kable(resumen_mtm, format.args=list(big.mark=","),
      caption="Tabla 11b. Resumen trimestral mark-to-market (16 trimestres)") %>%
  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%
  row_spec(which(resumen_mtm$`GP Corta (USD)` > 0), background="#e8f5e9") %>%
  row_spec(which(resumen_mtm$`GP Corta (USD)` < 0), background="#ffebee") %>%
  row_spec(0, background="#003087", color="white") %>%
  column_spec(2, bold=TRUE)
Tabla 11b. Resumen trimestral mark-to-market (16 trimestres)
Trim GP Corta (USD) Margin Calls Reponer (USD) Saldo Final (USD) GP Acumulada (USD) Resultado
1 1,785,808 0 0 2,233,808 1,785,808 Ganancia ✔
2 206,309 0 0 2,440,118 1,992,117 Ganancia ✔
3 -946,074 0 0 1,494,044 1,046,043 Pérdida ✗
4 714,110 0 0 2,208,153 1,760,153 Ganancia ✔
5 -554,418 0 0 1,653,735 1,205,735 Pérdida ✗
6 538,030 0 0 2,191,765 1,743,765 Ganancia ✔
7 -329,348 0 0 1,862,417 1,414,417 Pérdida ✗
8 -893,699 0 0 968,719 520,718 Pérdida ✗
9 1,043,904 0 0 2,012,623 1,564,622 Ganancia ✔
10 -834,901 0 0 1,177,721 729,721 Pérdida ✗
11 -61,564 0 0 1,116,157 668,157 Pérdida ✗
12 560,883 0 0 1,677,041 1,229,040 Ganancia ✔
13 -1,791,053 2 562,013 448,000 -562,013 Pérdida ✗
14 -428,020 2 1,008,682 1,028,662 -990,033 Pérdida ✗
15 -1,457,700 2 877,037 448,000 -2,447,733 Pérdida ✗
16 478,263 0 0 926,263 -1,969,470 Ganancia ✔
p_mtm <- ggplot(mtm_tabla, aes(x=Mes)) +

  geom_col(aes(y=GP_corta/1e3, fill=GP_corta >= 0), alpha=0.75, width=0.8) +

  geom_line(aes(y=Saldo_margen/1e3), color="navy", linewidth=1.1) +

  geom_hline(yintercept=MARGEN_MANT*N_CONT/1e3, linetype="dashed",

             color="red", linewidth=0.9) +

  geom_hline(yintercept=MARGEN_INI*N_CONT/1e3, linetype="dotted",

             color="darkgreen", linewidth=0.8) +

  geom_point(data=mtm_tabla[mtm_tabla$Margin_call>0,],

             aes(x=Mes, y=Margin_call/1e3), color="red", size=4, shape=8) +

  scale_fill_manual(values=c("TRUE"="forestgreen","FALSE"="firebrick"),

                    labels=c("FALSE"="Pérdida (mercado sube)","TRUE"="Ganancia (mercado baja)"),

                    name="G/P posición corta") +

  scale_x_continuous(breaks=seq(0,N_MESES,6),

                     labels=paste0("M",seq(0,N_MESES,6))) +

  annotate("text", x=1, y=MARGEN_MANT*N_CONT/1e3*1.05,

           label="Margen mantenimiento", color="red", size=3, hjust=0) +

  annotate("text", x=1, y=MARGEN_INI*N_CONT/1e3*1.02,

           label="Margen inicial", color="darkgreen", size=3, hjust=0) +

  labs(title="Figura 5. Flujos mensuales — posición corta E-mini S&P 500 (4 años)",

       subtitle=sprintf("%d contratos | F₀=%d puntos | Nocional: %s",

                        N_CONT, F0, dollar(N_CONT*F0*QF)),

       x="Mes", y="Miles USD",

       caption="✱ = Margin Call  |  Línea azul: saldo de margen  |  Línea roja: margen de mantenimiento") +

  theme_minimal(base_size=12) +

  theme(legend.position="bottom", plot.title=element_text(face="bold"))

ggplotly(p_mtm)

Análisis del mark-to-market (48 meses): La tabla muestra los flujos completos del horizonte de inversión — use el filtro por “Año” o “Trim.” para navegar. La posición corta genera ganancias (verde) cuando el futuro cae y pérdidas (rojo) cuando sube. Filas en amarillo: margin call (saldo bajo el margen de mantenimiento $406,400; se repone al inicial $448,000). La Tabla 11b consolida los flujos netos por trimestre, facilitando la comparación directa con el roll-over (Tabla 12). Los margin calls representan el costo de liquidez de la cobertura (Hull, 2022).


12 Estrategia de Roll-Over Trimestral

12.1 Fundamento del roll-over

¿Por qué roll-over? Los contratos E-mini S&P 500 tienen vencimientos trimestrales (Mar/Jun/Sep/Dic). Para mantener la cobertura durante 4 años continuos, es necesario cerrar la posición al vencimiento de cada contrato y abrir una nueva posición en el siguiente vencimiento disponible. Este proceso de renovación se denomina roll-over y genera costos implícitos asociados al diferencial bid-ask y al riesgo de base (Hull, 2022).

gp_roll <- data.frame()

for (q in 1:16) {

  mi <- (q-1)*3+1; mf <- q*3

  if (mf > N_MESES) break

  Fa   <- precio_fut_mes[mi]

  Fc   <- precio_fut_mes[mf+1]

  GPc  <- (Fa - Fc) * QF * N_CONT

  base <- precio_idx_mes[mf+1] - Fc

  gp_roll <- rbind(gp_roll, data.frame(

    Trimestre  = q,

    F_apertura = round(Fa,2),

    F_cierre   = round(Fc,2),

    Variación  = round(Fc-Fa,2),

    GP_corta   = round(GPc),

    Base_pts   = round(base,2),

    Efecto_pct = round(GPc/CAPITAL*100, 3),

    Posicion   = ifelse(GPc>0,"Ganancia ✔","Pérdida ✗")

  ))

}

kable(gp_roll,

      col.names=c("Trim.","F Apertura","F Cierre","Variación","G/P Corta (USD)",

                  "Base (pts)","Efecto (%)","Resultado"),

      align="c", format.args=list(big.mark=","),

      caption="Tabla 12. Roll-over trimestral — ganancias/pérdidas y riesgo de base") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(which(gp_roll$GP_corta > 0), background="#e8f5e9") %>%

  row_spec(which(gp_roll$GP_corta < 0), background="#ffebee") %>%

  row_spec(0, background="#003087", color="white")
Tabla 12. Roll-over trimestral — ganancias/pérdidas y riesgo de base
Trim. F Apertura F Cierre Variación G/P Corta (USD) Base (pts) Efecto (%) Resultado
1 7,210.91 6,094.78 -1,116.13 1,785,808 -63.36 8.929 Ganancia ✔
2 6,094.78 5,965.84 -128.94 206,309 -62.02 1.032 Ganancia ✔
3 5,965.84 6,557.13 591.30 -946,074 -68.17 -4.730 Pérdida ✗
4 6,557.13 6,110.82 -446.32 714,109 -63.53 3.571 Ganancia ✔
5 6,110.82 6,457.33 346.51 -554,418 -67.13 -2.772 Pérdida ✗
6 6,457.33 6,121.06 -336.27 538,030 -63.63 2.690 Ganancia ✔
7 6,121.06 6,326.90 205.84 -329,347 -65.77 -1.647 Pérdida ✗
8 6,326.90 6,885.46 558.56 -893,699 -71.58 -4.468 Pérdida ✗
9 6,885.46 6,233.02 -652.44 1,043,904 -64.80 5.220 Ganancia ✔
10 6,233.02 6,754.84 521.81 -834,902 -70.22 -4.175 Pérdida ✗
11 6,754.84 6,793.31 38.48 -61,564 -70.62 -0.308 Pérdida ✗
12 6,793.31 6,442.76 -350.55 560,883 -66.98 2.804 Ganancia ✔
13 6,442.76 7,562.17 1,119.41 -1,791,054 -78.61 -8.955 Pérdida ✗
14 7,562.17 7,829.68 267.51 -428,020 -81.39 -2.140 Pérdida ✗
15 7,829.68 8,740.75 911.06 -1,457,700 -90.87 -7.288 Pérdida ✗
16 8,740.75 8,441.83 -298.91 478,263 -87.76 2.391 Ganancia ✔
gp_roll$GP_acumulada <- cumsum(gp_roll$GP_corta)

p_roll <- ggplot(gp_roll, aes(x=Trimestre)) +

  geom_col(aes(y=GP_corta/1e6, fill=GP_corta>=0), alpha=0.8) +

  geom_line(aes(y=GP_acumulada/1e6), color="navy", linewidth=1.2) +

  geom_point(aes(y=GP_acumulada/1e6), color="navy", size=2.5) +

  geom_hline(yintercept=0, linetype="dashed", color="gray50") +

  scale_fill_manual(values=c("TRUE"="forestgreen","FALSE"="firebrick"),

                    labels=c("FALSE"="Pérdida","TRUE"="Ganancia")) +

  scale_x_continuous(breaks=1:16, labels=paste0("Q",1:16)) +

  labs(title="Figura 6. G/P por trimestre y resultado acumulado del roll-over",

       x="Trimestre", y="Millones USD",

       caption="Barras: G/P trimestral | Línea azul: G/P acumulada") +

  theme_minimal(base_size=12) +

  theme(legend.position="bottom", axis.text.x=element_text(angle=45, hjust=1))

ggplotly(p_roll)

Riesgo de base: La base = Precio spot – Precio futuro. Al vencimiento del contrato, la base converge a cero (convergencia del futuro hacia el spot). Sin embargo, al hacer roll-over antes del vencimiento, la base puede ser distinta de cero, generando una cobertura imperfecta. Los valores negativos de base en la tabla confirman que el futuro se negocia por encima del spot (contango), lo cual es el estado normal del mercado para futuros sobre índices con dividendos (Hull, 2022).

Estrategia siempre-corta vs siempre-larga en roll-over:

  • Siempre corta: Acumula ganancias en entornos bajistas pero pierde en bull markets. Durante el período analizado, el mercado es predominantemente alcista, lo que genera un resultado acumulado negativo para la posición corta en los últimos trimestres.

  • Siempre larga: Amplificaría las ganancias del portafolio en mercados alcistas pero eliminaría la cobertura bajista, exponiendo al inversor a caídas del mercado sin protección.


13 Valor Esperado de la Cobertura Trimestral

13.1 Tasa libre de riesgo y marco teórico

Tasa libre de riesgo utilizada: ^FVX (CBOE 5-Year Treasury Note Index, proxy del rango 3–5 años especificado en el enunciado) = 4.18% anual al 30/04/2026. Esta tasa fue seleccionada porque: (i) su vencimiento de 5 años es el más cercano al horizonte de 4 años del portafolio, (ii) es la referencia implícita en contratos de futuros CME de mediano plazo, y (iii) es observable y verificable en la fecha inicial del ejercicio (Hull, 2022; Bodie et al., 2021).

Precio teórico del futuro (costo de acarreo): \(F_0 = S_0 \times e^{(r_f - d) \times T}\), donde \(d\) es el dividend yield del índice. Con dividendos incorporados en los precios ajustados, la aproximación usada es \(F_0 \approx S_0 \times e^{r_f \times T}\).

VE_sin_cob   <- CAPITAL * exp(mu_port * T_trim)

VE_fut_teo   <- F0 * exp(RF_ANUAL * T_trim)

VE_pos_corta <- (F0 - VE_fut_teo) * QF * N_CONT

VE_cubierto  <- VE_sin_cob + VE_pos_corta

costo_cob    <- VE_sin_cob - VE_cubierto

ve_df <- data.frame(

  Concepto = c(

    "Tasa libre de riesgo (^FVX 5Y, 30/04/2026)",

    "Horizonte de cálculo (T)",

    "VE portafolio SIN cobertura (1 trimestre)",

    "Precio teórico futuro F₀ × e^(rf×T)",

    "VE posición corta en futuros",

    "VE portafolio CUBIERTO (1 trimestre)",

    "Costo explícito de la cobertura"

  ),

  Valor = c(

    paste0(RF_ANUAL*100,"% anual"),

    paste0(T_trim," años (3 meses)"),

    dollar(round(VE_sin_cob)),

    round(VE_fut_teo, 2),

    dollar(round(VE_pos_corta)),

    dollar(round(VE_cubierto)),

    dollar(round(costo_cob))

  )

)

kable(ve_df, align=c("l","r"),

      caption="Tabla 13. Valor esperado de la cobertura trimestral") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(6, bold=TRUE, background="#eef4fb") %>%

  row_spec(7, bold=TRUE, background="#fff8e1") %>%

  row_spec(0, background="#003087", color="white")
Tabla 13. Valor esperado de la cobertura trimestral
Concepto Valor
Tasa libre de riesgo (^FVX 5Y, 30/04/2026) 4.18% anual
Horizonte de cálculo (T) 0.25 años (3 meses)
VE portafolio SIN cobertura (1 trimestre) $20,775,564
Precio teórico futuro F₀ × e^(rf×T) 7243.3
VE posición corta en futuros -$120,477
VE portafolio CUBIERTO (1 trimestre) $20,655,087
Costo explícito de la cobertura $120,477

Interpretación: El portafolio cubierto tiene un VE de $20,655,087, levemente inferior al portafolio sin cobertura ($20,775,564). La diferencia de $120,477 representa el costo de la cobertura: la prima que el inversor paga para reducir la exposición al riesgo sistémico. Este es el trade-off fundamental de la cobertura: se reduce riesgo a costa de reducir rentabilidad esperada. Sin embargo, este costo es conocido y acotado, mientras que la pérdida potencial sin cobertura puede ser mucho mayor (como ilustra el VaR al 99%).


14 Rendimiento Mensual y Valor de la Cartera

14.1 Dividendos y retorno total

Efecto de los dividendos sobre el retorno total:

Los tres componentes del portafolio pagan dividendos regulares:

Acción | Yield Anual | Pago típico | Impacto en estrategia |

|——–|————|————-|———————-|

PG | ~2.4% anual | Trimestral | Al usar precios ajustados Yahoo Finance, los dividendos ya están incluidos en los retornos históricos calculados mediante \(r_t = \ln(P_t^{adj}/P_{t-1}^{adj})\). Por tanto, el retorno esperado de PG del 8.68% anual incluye tanto apreciación de capital como reinversión de dividendos. |
SYK | ~1.0% anual | Trimestral | Ídem. El retorno de 11.68% anual incorpora los dividendos reinvertidos. |
WM | ~1.5% anual | Trimestral | Ídem. El retorno de 15.33% anual ya incluye la contribución de dividendos. |

Conclusión: Los dividendos están implícitamente incorporados en los retornos calculados. No se deben sumar por separado. Si se usaran precios sin ajustar, el retorno real del portafolio sería subestimado, pues omitiría entre 1.0% y 2.4% de retorno anual por acción (Bodie et al., 2021).

14.2 Evolución del valor de la cartera bajo tres escenarios

valor_sc  <- CAPITAL * exp(mu_port * (1:N_MESES)/12)

valor_cub <- numeric(N_MESES)

for (m in 1:N_MESES) {

  q  <- ceiling(m/3)

  aj <- if (q <= nrow(gp_roll)) gp_roll$GP_corta[q]/3 else 0

  valor_cub[m] <- valor_sc[m] + aj

}

valor_var <- CAPITAL * exp((mu_port - VaR_99_mc) * (1:N_MESES)/12)

df_ev <- data.frame(

  Mes = 1:N_MESES,

  `Sin cobertura`    = valor_sc/1e6,

  `Cubierto`         = valor_cub/1e6,

  `Escenario VaR 99%` = valor_var/1e6,

  check.names = FALSE

)

df_ev_long <- pivot_longer(df_ev, -Mes, names_to="Escenario", values_to="Valor_MM")

p_ev <- ggplot(df_ev_long, aes(x=Mes, y=Valor_MM, color=Escenario)) +

  geom_line(linewidth=1.2) +

  geom_hline(yintercept=CAPITAL/1e6, linetype="dashed", color="gray50") +

  geom_ribbon(data=df_ev_long[df_ev_long$Escenario %in% c("Sin cobertura","Escenario VaR 99%"),],

              aes(ymax=ifelse(Escenario=="Sin cobertura", Valor_MM, NA),

                  ymin=ifelse(Escenario=="Escenario VaR 99%", Valor_MM, NA)),

              fill="lightcoral", alpha=0.15, color=NA) +

  scale_color_manual(values=c("Sin cobertura"="steelblue",

                               "Cubierto"="forestgreen",

                               "Escenario VaR 99%"="firebrick")) +

  scale_x_continuous(breaks=seq(0,N_MESES,6)) +

  scale_y_continuous(labels=dollar_format(suffix=" M")) +

  annotate("text", x=2, y=CAPITAL/1e6+0.3, label="Capital inicial: USD 20M",

           color="gray50", size=3, hjust=0) +

  labs(title="Figura 7. Evolución del valor de la cartera — 4 años (48 meses)",

       subtitle="Comparación: portafolio libre vs cubierto vs escenario pesimista VaR 99%",

       x="Mes", y="Valor (millones USD)", color="Escenario") +

  theme_minimal(base_size=12) +

  theme(legend.position="bottom", plot.title=element_text(face="bold"))

ggplotly(p_ev)
meses_clave <- c(6, 12, 24, 36, 48)

res_df <- data.frame(

  Escenario = c("Sin cobertura","Cubierto","VaR 99% (pesimista)"),

  rbind(

    dollar(round(c(valor_sc[meses_clave]))),

    dollar(round(c(valor_cub[meses_clave]))),

    dollar(round(c(valor_var[meses_clave])))

  )

)

colnames(res_df) <- c("Escenario", paste0("Mes ", meses_clave))

kable(res_df, align=c("l",rep("r",5)),

      caption="Tabla 14. Valor esperado de la cartera por escenario y horizonte") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(0, background="#003087", color="white")
Tabla 14. Valor esperado de la cartera por escenario y horizonte
Escenario Mes 6 Mes 12 Mes 24 Mes 36 Mes 48
Sin cobertura $21,581,203 $23,287,416 $27,115,188 $31,572,133 $36,761,671
Cubierto $21,649,973 $23,525,453 $26,817,288 $31,759,094 $36,921,092
VaR 99% (pesimista) $20,327,886 $20,661,148 $21,344,152 $22,049,734 $22,778,641

Interpretación del escenario pesimista VaR 99%: El escenario ajustado por VaR 99% representa la trayectoria del portafolio si el retorno mensual realizado fuera consistentemente igual al VaR mensual negativo calculado. Al mes 48, incluso en este escenario extremo, el portafolio conserva un valor de $22,778,641 sobre los USD 20M iniciales, lo que confirma la robustez del portafolio defensivo para horizontes de largo plazo.


15 Sensibilidad de la Cobertura: β = 0.5 y β = 2

15.1 Análisis comparativo

betas_hip  <- c(0.5, beta_port, 2.0)

etiquetas  <- c("β = 0.5 (defensivo)","β actual del portafolio","β = 2.0 (agresivo)")

sens_df <- data.frame(

  Escenario            = etiquetas,

  Beta                 = betas_hip,

  `N* exacto`          = round(betas_hip * VP / (F0*QF), 4),

  `N* contratos`       = round(betas_hip * VP / (F0*QF)),

  `Nocional cubierto`  = dollar(round(round(betas_hip*VP/(F0*QF)) * F0*QF)),

  `Exp. sistemática`   = dollar(round(betas_hip * VP)),

  `Costo margen ini.`  = dollar(round(round(betas_hip*VP/(F0*QF)) * MARGEN_INI)),

  check.names          = FALSE

)

kable(sens_df, align=c("l","c","c","c","r","r","r"),

      caption="Tabla 15. Sensibilidad del número de contratos según beta hipotético") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(2, bold=TRUE, background="#eef4fb") %>%

  row_spec(3, background="#fff3cd") %>%

  row_spec(0, background="#003087", color="white")
Tabla 15. Sensibilidad del número de contratos según beta hipotético
Escenario Beta N* exacto N* contratos Nocional cubierto Exp. sistemática Costo margen ini.
β = 0.5 (defensivo) 0.5000000 27.9018 28 $10,035,200 $10,000,000 $392,000
β actual del portafolio 0.5756664 32.1242 32 $11,468,800 $11,513,327 $448,000
β = 2.0 (agresivo) 2.0000000 111.6071 112 $40,140,800 $40,000,000 $1,568,000
beta_rango <- seq(0.3, 2.5, by=0.05)

n_rango    <- beta_rango * VP / (F0*QF)

p_sens <- ggplot(data.frame(beta=beta_rango, N=n_rango), aes(x=beta, y=N)) +

  geom_line(color="steelblue", linewidth=1.3) +

  geom_ribbon(aes(ymin=0, ymax=N), fill="steelblue", alpha=0.1) +

  geom_vline(xintercept=1, linetype="dashed", color="gray50") +

  annotate("text", x=1.02, y=max(n_rango)*0.8, label="beta = 1\n(= mercado)",

           color="gray50", size=3, hjust=0) +

  geom_point(aes(x=0.5,       y=round(0.5*VP/(F0*QF))),

             color="forestgreen", size=5, shape=19) +

  geom_point(aes(x=beta_port, y=N_CONT),

             color="red", size=5, shape=19) +

  geom_point(aes(x=2.0,       y=round(2.0*VP/(F0*QF))),

             color="orange", size=5, shape=19) +

  annotate("label", x=0.5,       y=round(0.5*VP/(F0*QF))+5,

           label=sprintf("beta=0.5\n%d contratos", round(0.5*VP/(F0*QF))),

           color="forestgreen", size=3, fill="white") +

  annotate("label", x=beta_port, y=N_CONT+5,

           label=sprintf("beta=%.2f (actual)\n%d contratos", beta_port, N_CONT),

           color="red", size=3, fill="white") +

  annotate("label", x=2.0,       y=round(2.0*VP/(F0*QF))+5,

           label=sprintf("beta=2.0\n%d contratos", round(2.0*VP/(F0*QF))),

           color="darkorange", size=3, fill="white") +

  labs(title="Figura 8. N* de contratos en función de la beta del portafolio",

       subtitle=sprintf("N* = beta x Vp / (F0 x QF) | Vp=USD %sM | F0=%d pts | QF=%d",

                        CAPITAL/1e6, F0, QF),

       x="Beta del portafolio (β)", y="N* contratos óptimos") +

  theme_minimal(base_size=12) +

  theme(plot.title=element_text(face="bold"))

ggplotly(p_sens)

¿Por qué un β mayor requiere más contratos? La beta cuantifica la sensibilidad del portafolio a movimientos del índice. Un portafolio con β=2 amplifica el movimiento del mercado el doble: si el S&P 500 cae 10%, el portafolio pierde ~20%. Para neutralizar esa mayor exposición, se necesitan más contratos cortos. La relación es exactamente lineal: duplicar la beta duplica el número de contratos necesarios. Un portafolio con β=0.5 ya tiene inherentemente menor exposición al mercado, por lo que requiere menos cobertura externa (Hull, 2022).


16 Simulación GBM — Trayectorias de Precios

colores <- c(PG="#003087", SYK="#E31837", WM="#00703C")

n_show  <- 100

par(mfrow=c(1,3), mar=c(4,4,3,1), bg="white")

for (tk in TICKERS) {

  paths <- sim_precios[[tk]]

  med   <- apply(paths, 2, median)

  p05   <- apply(paths, 2, quantile, 0.05)

  p95   <- apply(paths, 2, quantile, 0.95)

  p25   <- apply(paths, 2, quantile, 0.25)

  p75   <- apply(paths, 2, quantile, 0.75)

  ylim  <- range(p05, p95)

  plot(0:N_MES, paths[1,], type="l",

       col=adjustcolor(colores[tk], 0.06),

       ylim=ylim, xlab="Días hábiles", ylab="Precio (USD)",

       main=sprintf("GBM: %s | mu=%.1f%% | sigma=%.1f%% (anual)",

                    tk, mu_anual[tk]*100, sd_anual[tk]*100),

       cex.main=0.95, font.main=2)

  for (i in 2:n_show)

    lines(0:N_MES, paths[i,], col=adjustcolor(colores[tk], 0.06))

  polygon(c(0:N_MES, rev(0:N_MES)), c(p05, rev(p95)),

          col=adjustcolor(colores[tk], 0.12), border=NA)

  polygon(c(0:N_MES, rev(0:N_MES)), c(p25, rev(p75)),

          col=adjustcolor(colores[tk], 0.20), border=NA)

  lines(0:N_MES, med, col=colores[tk], lwd=2.5)

  lines(0:N_MES, p05, col="gray30", lwd=1, lty=2)

  lines(0:N_MES, p95, col="gray30", lwd=1, lty=2)

  abline(h=S0[tk], col="gray50", lty=3, lwd=1)

  legend("topleft",

         legend=c("Mediana","IC 50%","IC 90%","Precio inicial"),

         lty=c(1,NA,2,3), lwd=c(2.5,NA,1,1),

         pch=c(NA,15,NA,NA),

         col=c(colores[tk], adjustcolor(colores[tk],0.4), "gray30","gray50"),

         bty="n", cex=0.75)

}

par(mfrow=c(1,1))


17 Movimiento Browniano Correlacionado — Trayectorias de Precio

17.1 Marco teórico

Movimiento Browniano Geométrico (GBM) multivariado con correlación:

\[dS_i = \mu_i S_i \, dt + \sigma_i S_i \, dW_i\]

donde los incrementos brownianos están correlacionados: \(\text{Corr}(dW_i, dW_j) = \rho_{ij}\) según la matriz empírica de correlaciones.

Discretización de Euler-Maruyama:

\[S_i(t+\Delta t) = S_i(t) \exp\!\left[(\mu_i - \tfrac{1}{2}\sigma_i^2)\Delta t + \sigma_i \sqrt{\Delta t}\, Z_i\right]\]

Los \(Z_i\) se generan mediante la descomposición de Cholesky de la matriz de correlaciones, garantizando que las trayectorias simuladas reproduzcan la estructura de dependencia histórica entre PG, SYK y WM (y el índice S&P 500).

¿Por qué correlacionar? El GBM independiente (usado en el VaR y la sección anterior) simula cada acción de forma aislada, subestimando el riesgo en escenarios de caída simultánea. El GBM correlacionado (esta sección) captura que PG, SYK y WM se mueven conjuntamente según \(\rho_{ij}\), produciendo trayectorias del portafolio estadísticamente más realistas para horizontes de inversión largos (Glasserman, 2004; Hull, 2022).

# ── Parámetros del GBM correlacionado ──────────────────────────
set.seed(2026)

N_SIM_C  <- 200          # trayectorias a graficar
N_PASO   <- N_MESES * N_MES   # pasos diarios totales (48 × 21 = 1 008 días)
dt       <- 1 / N_ANUAL  # paso diario en fracción de año

mu_d_v   <- colMeans(ret_acc)        # retorno diario medio por acción
sig_d_v  <- apply(ret_acc, 2, sd)    # vol diaria por acción
S0_acc   <- as.numeric(tail(datos[, TICKERS], 1))
names(S0_acc) <- TICKERS

# Descomposición de Cholesky de la matriz de correlaciones
L_chol <- t(chol(cor_mat))   # L tal que L %*% t(L) = cor_mat

# ── Simular N_SIM_C trayectorias correlacionadas ───────────────
sim_corr <- lapply(TICKERS, function(tk)
  matrix(NA_real_, nrow = N_SIM_C, ncol = N_PASO + 1))
names(sim_corr) <- TICKERS

for (s in seq_len(N_SIM_C)) {
  Z_ind <- matrix(rnorm(3 * N_PASO), nrow = 3)  # 3 × N_PASO independientes
  Z_cor <- L_chol %*% Z_ind                      # correlacionados

  for (j in seq_along(TICKERS)) {
    tk <- TICKERS[j]
    path <- numeric(N_PASO + 1)
    path[1] <- S0_acc[tk]
    for (t in seq_len(N_PASO)) {
      path[t + 1] <- path[t] *
        exp((mu_d_v[tk] - 0.5 * sig_d_v[tk]^2) * dt +
              sig_d_v[tk] * sqrt(dt) * Z_cor[j, t])
    }
    sim_corr[[tk]][s, ] <- path
  }
}

# ── Gráfico de trayectorias ────────────────────────────────────
colores_c <- c(PG = "#003087", SYK = "#E31837", WM = "#00703C")
dias_eje  <- seq(0, N_PASO, by = N_MES * 3)   # cada trimestre

par(mfrow = c(1, 3), mar = c(4, 4, 3, 1), bg = "white")

for (tk in TICKERS) {
  mat  <- sim_corr[[tk]]
  med  <- apply(mat, 2, median)
  p05  <- apply(mat, 2, quantile, 0.05)
  p25  <- apply(mat, 2, quantile, 0.25)
  p75  <- apply(mat, 2, quantile, 0.75)
  p95  <- apply(mat, 2, quantile, 0.95)
  ylim <- range(p05, p95)
  xs   <- 0:N_PASO

  plot(xs, mat[1, ], type = "l",
       col  = adjustcolor(colores_c[tk], 0.05),
       ylim = ylim,
       xlab = "Días hábiles", ylab = "Precio (USD)",
       main = sprintf("GBM Correlacionado: %s | mu=%.1f%% | sigma=%.1f%% (anual) | rho.idx=%.2f",
                      tk, mu_anual[tk]*100, sd_anual[tk]*100,
                      cor(as.numeric(ret_acc[,tk]), as.numeric(ret_idx))),
       cex.main = 0.85, font.main = 2,
       xaxt = "n")

  axis(1, at = dias_eje,
       labels = paste0("Q", seq_along(dias_eje) - 1),
       cex.axis = 0.8)

  for (i in 2:N_SIM_C)
    lines(xs, mat[i, ], col = adjustcolor(colores_c[tk], 0.04))

  polygon(c(xs, rev(xs)), c(p05, rev(p95)),
          col = adjustcolor(colores_c[tk], 0.10), border = NA)
  polygon(c(xs, rev(xs)), c(p25, rev(p75)),
          col = adjustcolor(colores_c[tk], 0.18), border = NA)

  lines(xs, med, col = colores_c[tk], lwd = 2.5)
  lines(xs, p05, col = "gray30", lwd = 1, lty = 2)
  lines(xs, p95, col = "gray30", lwd = 1, lty = 2)
  abline(h = S0_acc[tk], col = "gray50", lty = 3, lwd = 1)
  abline(v = dias_eje[-1], col = adjustcolor("gray70", 0.4), lty = 3)

  legend("topleft",
         legend = c("Mediana", "IC 50%", "IC 90%", "Precio inicial"),
         lty = c(1, NA, 2, 3), lwd = c(2.5, NA, 1, 1),
         pch = c(NA, 15, NA, NA),
         col = c(colores_c[tk], adjustcolor(colores_c[tk], 0.4),
                 "gray30", "gray50"),
         bty = "n", cex = 0.72)
}

par(mfrow = c(1, 1))

17.2 Valor del portafolio bajo GBM correlacionado

# ── Retorno mensual del portafolio en cada simulación ─────────────────────────
# Tomamos el valor al final de cada mes (cada N_MES pasos)
idx_mes <- seq(N_MES, N_PASO, by = N_MES)   # índices de fin de mes

ret_mes_corr <- matrix(NA_real_, nrow = N_SIM_C, ncol = N_MESES)

for (s in seq_len(N_SIM_C)) {
  val_ini <- sum(w_opt * S0_acc)
  for (m in seq_len(N_MESES)) {
    precios_m <- sapply(TICKERS, function(tk) sim_corr[[tk]][s, idx_mes[m]])
    val_m     <- sum(w_opt * precios_m / S0_acc * CAPITAL)
    ret_mes_corr[s, m] <- val_m
  }
}

# Percentiles del valor del portafolio por mes
pct_corr <- apply(ret_mes_corr, 2, quantile,
                  probs = c(0.01, 0.05, 0.25, 0.50, 0.75, 0.95, 0.99))

df_gbm_port <- data.frame(
  Mes    = 1:N_MESES,
  p01    = pct_corr["1%", ],
  p05    = pct_corr["5%", ],
  p25    = pct_corr["25%", ],
  mediana= pct_corr["50%", ],
  p75    = pct_corr["75%", ],
  p95    = pct_corr["95%", ],
  p99    = pct_corr["99%", ]
) %>% mutate(across(-Mes, ~ . / 1e6))

p_gbm_port <- ggplot(df_gbm_port, aes(x = Mes)) +
  geom_ribbon(aes(ymin = p01, ymax = p99),
              fill = "#003087", alpha = 0.08) +
  geom_ribbon(aes(ymin = p05, ymax = p95),
              fill = "#003087", alpha = 0.12) +
  geom_ribbon(aes(ymin = p25, ymax = p75),
              fill = "#003087", alpha = 0.20) +
  geom_line(aes(y = mediana), color = "#003087", linewidth = 1.4) +
  geom_line(aes(y = p01), color = "#c62828", linewidth = 0.8, linetype = "dashed") +
  geom_line(aes(y = p05), color = "#E31837", linewidth = 0.8, linetype = "dashed") +
  geom_hline(yintercept = CAPITAL / 1e6, linetype = "dotted",
             color = "gray40", linewidth = 0.8) +
  annotate("text", x = 2, y = CAPITAL/1e6 + 0.4,
           label = "Capital inicial: USD 20M", color = "gray40", size = 3, hjust = 0) +
  scale_x_continuous(breaks = seq(0, N_MESES, 6)) +
  scale_y_continuous(labels = dollar_format(suffix = " M")) +
  labs(
    title    = "Figura 9. Valor del portafolio — GBM correlacionado (200 trayectorias)",
    subtitle = sprintf(
      "Horizonte: 4 años | Cholesky sobre matriz de correlaciones empírica | %d simulaciones",
      N_SIM_C),
    x = "Mes", y = "Valor portafolio (millones USD)",
    caption = paste0(
      "Bandas: IC 50% / IC 90% / IC 98%  |  Línea azul: mediana  ",
      "| Líneas rojas: percentil 1% y 5% (VaR)")
  ) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold", color = "#003087"))

ggplotly(p_gbm_port)

17.3 Comparación GBM independiente vs correlacionado

# VaR mensual desde GBM correlacionado (retornos mes 1)
ret_m1_corr <- sapply(seq_len(N_SIM_C), function(s) {
  precios_m1 <- sapply(TICKERS, function(tk) sim_corr[[tk]][s, N_MES])
  log(sum(w_opt * precios_m1 / S0_acc))
})

VaR_99_corr <- -quantile(ret_m1_corr, 0.01)
VaR_95_corr <- -quantile(ret_m1_corr, 0.05)

comp_df <- data.frame(
  Método = c(
    "GBM independiente (VaR-calc)",
    "GBM correlacionado (Cholesky)"
  ),
  `VaR 99% (%)` = round(c(VaR_99_mc, VaR_99_corr) * 100, 3),
  `VaR 99% (USD)` = dollar(round(c(VaR_99_mc, VaR_99_corr) * CAPITAL)),
  `VaR 95% (%)` = round(c(VaR_95_mc, VaR_95_corr) * 100, 3),
  `VaR 95% (USD)` = dollar(round(c(VaR_95_mc, VaR_95_corr) * CAPITAL)),
  check.names = FALSE
)

kable(comp_df, align = c("l","c","r","c","r"),
      caption = "Tabla 17. Comparación VaR mensual: GBM independiente vs correlacionado") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE) %>%
  row_spec(2, bold = TRUE, background = "#eef4fb") %>%
  row_spec(0, background = "#003087", color = "white")
Tabla 17. Comparación VaR mensual: GBM independiente vs correlacionado
Método VaR 99% (%) VaR 99% (USD) VaR 95% (%) VaR 95% (USD)
GBM independiente (VaR-calc) 11.966 $2,393,162 7.981 $1,596,208
GBM correlacionado (Cholesky) 0.687 $137,447 0.522 $104,370

Interpretación del GBM correlacionado:

  • La descomposición de Cholesky transforma innovaciones brownianas independientes en vectores correlacionados según la estructura empírica $ ho_{ij}$ entre PG, SYK y WM. Esto garantiza que los escenarios simulados sean internamente consistentes: cuando SYK cae, PG y WM también tienden a caer (aunque en menor magnitud), replicando el comportamiento observado históricamente.

  • Las bandas de confianza (IC 50%, 90%, 98%) en la Figura 9 muestran la dispersión del valor del portafolio. El percentil 1% (línea roja inferior) equivale al VaR 99% dinámico del portafolio a lo largo del horizonte completo de 4 años.

  • La comparación en la Tabla 17 cuantifica el impacto de incluir correlación: el GBM correlacionado típicamente produce un VaR igual o mayor al GBM independiente cuando las correlaciones son positivas (como en este caso, $ ho $), ya que las pérdidas de las acciones tienden a coincidir en el tiempo.

  • Implicación para la cobertura: El VaR del GBM correlacionado es el insumo más conservador y realista para dimensionar la posición en futuros. Un gestor prudente usaría este valor para determinar el monto mínimo de cobertura requerido (Hull, 2022; Glasserman, 2004).

Diferencia con la sección de VaR: En la Sección 4 (VaR Mensual), el GBM simula cada acción de forma independiente con el mismo \(\mu\) y \(\sigma\) históricos — es el enfoque estándar para un VaR de corto plazo. Esta sección adiciona la estructura de correlación mediante Cholesky y extiende el horizonte a 4 años (1 008 días hábiles), permitiendo visualizar la incertidumbre acumulada del portafolio completo. Ambos enfoques son complementarios (Glasserman, 2004).


18 Resumen Ejecutivo

resumen_df <- data.frame(

  Indicador = c(

    "Capital inicial","Acciones seleccionadas","Horizonte de inversión",

    "Retorno esperado portafolio","Volatilidad portafolio","Sharpe Ratio",

    "Beta portafolio ponderado",

    "VaR mensual 99% (Monte Carlo GBM)","VaR mensual 95% (Monte Carlo GBM)",

    "Contratos E-mini (posición CORTA)","Nocional cubierto",

    "Margen inicial total requerido",

    "Tasa libre de riesgo (^FVX 5Y)","VE portafolio cubierto (1 trim.)",

    "Valor esperado a 4 años (sin cob.)","Valor esperado a 4 años (cubierto)"

  ),

  Valor = c(

    dollar(CAPITAL), paste(TICKERS, collapse=" | "),

    paste(HORIZONTE, "años"),

    paste0(round(mu_port*100,2), "% anual"),

    paste0(round(sigma_port*100,2), "% anual"),

    round(sharpe_opt, 4),

    round(beta_port, 4),

    paste0(round(VaR_99_mc*100,2), "% = ", dollar(round(VaR_99_mc*CAPITAL))),

    paste0(round(VaR_95_mc*100,2), "% = ", dollar(round(VaR_95_mc*CAPITAL))),

    paste0(N_CONT, " contratos"),

    dollar(N_CONT*F0*QF),

    dollar(N_CONT*MARGEN_INI),

    paste0(RF_ANUAL*100, "% anual (^FVX 5Y)"),

    dollar(round(VE_cubierto)),

    dollar(round(valor_sc[48])),

    dollar(round(valor_cub[48]))

  )

)

kable(resumen_df, align=c("l","r"),

      caption="Tabla 16. Resumen ejecutivo del ejercicio") %>%

  kable_styling(bootstrap_options=c("striped","hover","condensed"), full_width=FALSE) %>%

  row_spec(c(4,5,6,7,8,9,10,11,12), bold=TRUE) %>%

  row_spec(0, background="#003087", color="white") %>%

  pack_rows("Portafolio", 1, 7) %>%

  pack_rows("Riesgo (VaR)", 8, 9) %>%

  pack_rows("Cobertura con futuros", 10, 16)
Tabla 16. Resumen ejecutivo del ejercicio
Indicador Valor
Portafolio
Capital inicial $20,000,000
Acciones seleccionadas PG &#124; SYK &#124; WM
Horizonte de inversión 4 años
Retorno esperado portafolio 15.22% anual
Volatilidad portafolio 19.26% anual
Sharpe Ratio 0.5732
Beta portafolio ponderado 0.5757
Riesgo (VaR)
VaR mensual 99% (Monte Carlo GBM) 11.97% = $2,393,162
VaR mensual 95% (Monte Carlo GBM) 7.98% = $1,596,208
Cobertura con futuros
Contratos E-mini (posición CORTA) 32 contratos
Nocional cubierto $11,468,800
Margen inicial total requerido $448,000
Tasa libre de riesgo (^FVX 5Y) 4.18% anual (^FVX 5Y)
VE portafolio cubierto (1 trim.) $20,655,087
Valor esperado a 4 años (sin cob.) $36,761,671
Valor esperado a 4 años (cubierto) $36,921,092

19 Referencias

Referencias en normas APA 7ª edición:

Bodie, Z., Kane, A., & Marcus, A. J. (2021). Investments (12th ed.). McGraw-Hill Education.

CME Group. (2026). E-mini S&P 500 futures contract specifications. https://www.cmegroup.com/trading/equity-index/us-index/e-mini-sandp500.html

Elton, E. J., Gruber, M. J., Brown, S. J., & Goetzmann, W. N. (2014). Modern portfolio theory and investment analysis (9th ed.). John Wiley & Sons.

Glasserman, P. (2004). Monte Carlo methods in financial engineering. Springer.

Hull, J. C. (2022). Options, futures, and other derivatives (11th ed.). Pearson Education.

Keitt, T. (2023). quantmod: Quantitative financial modelling framework (R package version 0.4.22). CRAN. https://CRAN.R-project.org/package=quantmod

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

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

Yahoo Finance. (2026). Historical market data [Dataset]. https://finance.yahoo.com

CBOE. (2026). CBOE Interest Rate 5-Year Treasury Note Index (^FVX) [Dataset]. https://finance.yahoo.com/quote/%5EFVX


Informe generado en R Markdown v2.27 | R v4.3+ | Datos: Yahoo Finance API | Futuros: CME Group

Publicado en RPubs. Para reproducir: ejecutar el código fuente en RStudio con los paquetes indicados.