Introducción

El presente trabajo tiene como objetivo construir un portafolio óptimo de inversión utilizando tres acciones pertenecientes al índice S&P 500 y posteriormente desarrollar una estrategia de cobertura mediante contratos de futuros sobre dicho índice.

El horizonte de inversión es de cuatro años iniciando el 30 de abril de 2026 con un capital inicial de USD 20.000.000.

# Librerías

library(quantmod)
## Loading required package: xts
## Warning: package 'xts' was built under R version 4.3.3
## Loading required package: zoo
## Warning: package 'zoo' was built under R version 4.3.3
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## Loading required package: TTR
## Warning: package 'TTR' was built under R version 4.3.3
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
library(PerformanceAnalytics)
## Warning: package 'PerformanceAnalytics' was built under R version 4.3.3
## 
## Attaching package: 'PerformanceAnalytics'
## The following object is masked from 'package:graphics':
## 
##     legend
library(PortfolioAnalytics)
## Warning: package 'PortfolioAnalytics' was built under R version 4.3.3
## Loading required package: foreach
## Warning: package 'foreach' was built under R version 4.3.3
## Registered S3 method overwritten by 'PortfolioAnalytics':
##   method           from
##   print.constraint ROI
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.3.3
## Warning: package 'tibble' was built under R version 4.3.3
## Warning: package 'tidyr' was built under R version 4.3.3
## Warning: package 'readr' was built under R version 4.3.3
## Warning: package 'purrr' was built under R version 4.3.3
## Warning: package 'dplyr' was built under R version 4.3.3
## Warning: package 'lubridate' was built under R version 4.3.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.2     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ purrr::accumulate() masks foreach::accumulate()
## ✖ dplyr::filter()     masks stats::filter()
## ✖ dplyr::first()      masks xts::first()
## ✖ dplyr::lag()        masks stats::lag()
## ✖ dplyr::last()       masks xts::last()
## ✖ purrr::when()       masks foreach::when()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ROI)
## ROI: R Optimization Infrastructure
## Registered solver plugins: nlminb, symphony, glpk, quadprog.
## Default solver: auto.
## 
## Attaching package: 'ROI'
## 
## The following objects are masked from 'package:PortfolioAnalytics':
## 
##     is.constraint, objective
library(ROI.plugin.quadprog)
## Warning: package 'ROI.plugin.quadprog' was built under R version 4.3.3
library(knitr)
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
# Parámetros del ejercicio

capital_inicial <- 20000000

fecha_inicio <- "2016-04-30"
fecha_fin <- "2026-04-29"

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

indice <- "^GSPC"


# Descarga de datos históricos

getSymbols(c(acciones, indice),
           src = "yahoo",
           from = fecha_inicio,
           to = fecha_fin)
## [1] "PG"   "SYK"  "WM"   "GSPC"
precios <- na.omit(merge(
  Ad(PG),
  Ad(SYK),
  Ad(WM),
  Ad(GSPC)
))

colnames(precios) <- c("PG","SYK","WM","SP500")

head(precios)
##                  PG      SYK       WM   SP500
## 2016-05-02 61.64122 98.37418 49.96725 2081.43
## 2016-05-03 61.74017 97.68515 49.85063 2063.37
## 2016-05-04 62.12084 97.16618 50.32547 2051.12
## 2016-05-05 61.89240 98.53521 50.23383 2050.63
## 2016-05-06 62.52430 99.51054 50.55872 2057.14
## 2016-05-09 62.51672 99.60896 50.98360 2058.69
# Rendimientos logarítmicos


rendimientos <- na.omit(Return.calculate(precios,
                                         method = "log"))

head(rendimientos)
##                       PG           SYK           WM         SP500
## 2016-05-03  0.0016039607 -0.0070287778 -0.002336639 -0.0087144994
## 2016-05-04  0.0061467256 -0.0053268873  0.009480217 -0.0059545827
## 2016-05-05 -0.0036841160  0.0139912770 -0.001822692 -0.0002390367
## 2016-05-06  0.0101578999  0.0098496639  0.006446807  0.0031696106
## 2016-05-09 -0.0001213591  0.0009884674  0.008368523  0.0007532133
## 2016-05-10  0.0043741348  0.0038553964  0.002447798  0.0124063652
# Estadísticas descriptivas

## Retornos esperados anualizados


retornos_esperados <- colMeans(rendimientos[,1:3]) * 252

kable(data.frame(
  Acción = names(retornos_esperados),
  Retorno_Anual = round(retornos_esperados,4)
))
Acción Retorno_Anual
PG PG 0.0887
SYK SYK 0.1188
WM WM 0.1521
## Matriz de covarianza

covarianza <- cov(rendimientos[,1:3]) * 252

kable(round(covarianza,6))
PG SYK WM
PG 0.035716 0.019230 0.018355
SYK 0.019230 0.068346 0.023956
WM 0.018355 0.023956 0.037875
# Optimización media-varianza


portafolio <- portfolio.spec(assets = acciones)

portafolio <- add.constraint(
  portfolio = portafolio,
  type = "full_investment"
)

portafolio <- add.constraint(
  portfolio = portafolio,
  type = "long_only"
)

portafolio <- add.objective(
  portfolio = portafolio,
  type = "risk",
  name = "var"
)

optimizacion <- optimize.portfolio(
  R = rendimientos[,1:3],
  portfolio = portafolio,
  optimize_method = "ROI"
)

pesos <- extractWeights(optimizacion)

pesos
##        PG       SYK        WM 
## 0.4826335 0.1162582 0.4011083
# Distribución óptima del capital


distribucion <- data.frame(
  Acción = names(pesos),
  Peso = round(pesos,4),
  Inversión_USD = round(pesos * capital_inicial,2)
)

kable(distribucion)
Acción Peso Inversión_USD
PG PG 0.4826 9652669
SYK SYK 0.1163 2325165
WM WM 0.4011 8022166
# Rendimiento y volatilidad del portafolio


retorno_portafolio <- sum(retornos_esperados * pesos)

volatilidad_portafolio <- sqrt(
  t(pesos) %*%
    covarianza %*%
    pesos
)

resultado_portafolio <- data.frame(
  Retorno_Esperado = round(retorno_portafolio,4),
  Volatilidad = round(volatilidad_portafolio,4)
)

kable(resultado_portafolio)
Retorno_Esperado Volatilidad
0.1176 0.1638
# Cálculo del VaR


nivel_confianza <- 0.95

z <- qnorm(1 - nivel_confianza)

VaR <- capital_inicial * (
  retorno_portafolio/252 +
    z * (volatilidad_portafolio/sqrt(252))
)

VaR
##           [,1]
## [1,] -330146.6
# Estimación de Betas CAPM

betas <- c()

mercado <- rendimientos$SP500

for(i in acciones){

  covarianza_beta <- cov(
    rendimientos[,i],
    mercado
  )

  beta <- covarianza_beta /
    var(mercado)

  betas[i] <- beta
}

kable(data.frame(
  Acción = names(betas),
  Beta = round(betas,4)
))
Acción Beta
PG PG 0.4849
SYK SYK 0.9701
WM WM 0.5633
# Beta del portafolio

beta_portafolio <- sum(
  pesos * betas
)

beta_portafolio
## [1] 0.5727506

Cobertura con futuros

Características del futuro E-mini S&P 500

Característica Valor
Activo subyacente S&P 500
Multiplicador 50 USD
Precio futuro inicial 5200
Valor nocional 260000 USD
Margen inicial 21867 USD
Margen mantenimiento 19879 USD
Liquidación Mark-to-market
Frecuencia práctica Diaria
Frecuencia académica Mensual

Número óptimo de contratos

precio_futuro <- 5200

multiplicador <- 50

valor_contrato <- precio_futuro * multiplicador

num_contratos <- (
  beta_portafolio *
  capital_inicial
) / valor_contrato

num_contratos
## [1] 44.05774
# Simulación mark-to-market mensual

set.seed(123)

meses <- 48

cambios <- rnorm(
  meses,
  mean = 0,
  sd = 0.03
)

precios_futuros <- c(precio_futuro)

for(i in 1:meses){

  nuevo_precio <- precios_futuros[i] *
    (1 + cambios[i])

  precios_futuros <- c(
    precios_futuros,
    nuevo_precio
  )
}

ganancias <- diff(precios_futuros) *
  multiplicador *
  round(num_contratos)

tabla_mtm <- data.frame(
  Mes = 1:meses,
  Precio_Futuro = round(precios_futuros[-1],2),
  Ganancia_Perdida = round(ganancias,2)
)

kable(head(tabla_mtm,12))
Mes Precio_Futuro Ganancia_Perdida
1 5112.57 -192355.24
2 5077.26 -77668.64
3 5314.68 522322.04
4 5325.92 24732.15
5 5346.58 45446.05
6 5621.67 605202.34
7 5699.41 171013.91
8 5483.10 -475866.42
9 5370.12 -248561.60
10 5298.32 -157955.05
11 5492.89 428048.29
12 5552.18 130443.57
# Evolución histórica de las acciones


chart.CumReturns(
  rendimientos[,1:3],
  legend.loc = "topleft",
  main = "Rendimientos acumulados"
)

Conclusiones

  1. El portafolio óptimo fue construido utilizando el enfoque media-varianza de Markowitz.

  2. Las acciones PG, SYK y WM pertenecen al índice S&P 500 y presentan diversificación sectorial.

  3. La estrategia de cobertura mediante futuros sobre el S&P 500 permite reducir el riesgo sistemático del portafolio.

  4. El cálculo del número óptimo de contratos se realizó utilizando la beta del portafolio.

  5. La estrategia incorpora ajustes mensuales mark-to-market y roll-over trimestral para mantener la cobertura durante el horizonte de inversión.

# Librerías necesarias
library(quantmod)
library(PerformanceAnalytics)
library(tidyverse)


# Retornos logarítmicos periódicos


retornos <- na.omit(Return.calculate(precios, method = "log"))
head(retornos) 
##                       PG           SYK           WM         SP500
## 2016-05-03  0.0016039607 -0.0070287778 -0.002336639 -0.0087144994
## 2016-05-04  0.0061467256 -0.0053268873  0.009480217 -0.0059545827
## 2016-05-05 -0.0036841160  0.0139912770 -0.001822692 -0.0002390367
## 2016-05-06  0.0101578999  0.0098496639  0.006446807  0.0031696106
## 2016-05-09 -0.0001213591  0.0009884674  0.008368523  0.0007532133
## 2016-05-10  0.0043741348  0.0038553964  0.002447798  0.0124063652
#Retorno promedio anual
# Supongamos datos diarios
frecuencia <- 252  # días hábiles en bolsa

retorno_anual_promedio <- colMeans(retornos) * frecuencia
retorno_anual_promedio
##         PG        SYK         WM      SP500 
## 0.08869233 0.11882452 0.15205534 0.12369071
# Desviación estándar anualizada
desviacion_anual <- apply(retornos, 2, sd) * sqrt(frecuencia)
desviacion_anual
##        PG       SYK        WM     SP500 
## 0.1889868 0.2614302 0.1946146 0.1811886
# Matriz de varianzas y covarianzas
matriz_covarianza <- cov(retornos) * frecuencia
matriz_covarianza
##               PG        SYK         WM      SP500
## PG    0.03571602 0.01923031 0.01835535 0.01591850
## SYK   0.01923031 0.06834573 0.02395604 0.03184708
## WM    0.01835535 0.02395604 0.03787486 0.01849309
## SP500 0.01591850 0.03184708 0.01849309 0.03282933
# Matriz de correlaciones
matriz_correlacion <- cor(retornos)
matriz_correlacion
##              PG       SYK        WM     SP500
## PG    1.0000000 0.3892235 0.4990632 0.4648786
## SYK   0.3892235 1.0000000 0.4708513 0.6723307
## WM    0.4990632 0.4708513 1.0000000 0.5244486
## SP500 0.4648786 0.6723307 0.5244486 1.0000000

Explicación de anualización

La anualización se realiza considerando una frecuencia de 252 días hábiles bursátiles por año.

Retornos promedio: se multiplican por 252 Desviación estándar: se multiplica por √252 Covarianza: se multiplica por 252 Correlación: no requiere anualización porque es adimensional

R <- na.omit(Return.calculate(precios, method="log"))

# Retornos anualizados
mu <- colMeans(R) * 252

# Volatilidad anualizada
sigma <- apply(R, 2, sd) * sqrt(252)

# Covarianza anualizada
cov_matrix <- cov(R) * 252

# Correlación
cor_matrix <- cor(R)

mu
##         PG        SYK         WM      SP500 
## 0.08869233 0.11882452 0.15205534 0.12369071
sigma
##        PG       SYK        WM     SP500 
## 0.1889868 0.2614302 0.1946146 0.1811886
cov_matrix
##               PG        SYK         WM      SP500
## PG    0.03571602 0.01923031 0.01835535 0.01591850
## SYK   0.01923031 0.06834573 0.02395604 0.03184708
## WM    0.01835535 0.02395604 0.03787486 0.01849309
## SP500 0.01591850 0.03184708 0.01849309 0.03282933