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
| 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 |
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"
)
El portafolio óptimo fue construido utilizando el enfoque media-varianza de Markowitz.
Las acciones PG, SYK y WM pertenecen al índice S&P 500 y presentan diversificación sectorial.
La estrategia de cobertura mediante futuros sobre el S&P 500 permite reducir el riesgo sistemático del portafolio.
El cálculo del número óptimo de contratos se realizó utilizando la beta del portafolio.
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