By: Oscar Walduin Orozco Cerón, Cod:2402067

1. Introducción

El modelo de media-varianza propuesto por Harry Markowitz en 1952 representa uno de los pilares fundamentales de la teoría moderna de portafolio. Su objetivo principal es construir combinaciones de activos que resulten eficientes en términos del binomio riesgo-retorno, es decir, seleccionar portafolios que, para un determinado nivel de riesgo, maximicen el retorno esperado, o, en sentido inverso, que minimicen el riesgo asociado a un nivel dado de retorno. Este enfoque parte del supuesto de que los inversionistas son racionales y aversos al riesgo, y que evalúan las oportunidades de inversión exclusivamente en función de la esperanza matemática del rendimiento y la dispersión de los resultados, medida a través de la varianza o desviación estándar.

Para poner en práctica este marco teórico, se trabajó con precios históricos diarios de un conjunto representativo de activos pertenecientes al sector tecnológico, extraídos directamente desde la plataforma Yahoo Finance. La recolección de datos se realizó mediante la función tq_get() de la librería tidyquant en R, que facilita la conexión entre herramientas de análisis financiero y fuentes de datos en tiempo real. El periodo de análisis abarca desde el 1 de enero de 2020 hasta el 7 de julio de 2025, lo que permite capturar tanto dinámicas pre y post-pandemia como recientes fluctuaciones asociadas a cambios macroeconómicos y tecnológicos globales.

Esta base de datos permitió estimar los rendimientos diarios de los activos, su media anualizada, y la matriz de covarianzas, insumos necesarios para simular múltiples combinaciones de portafolios aleatorios y trazar la frontera eficiente. A partir de esta simulación, fue posible identificar portafolios óptimos bajo distintos criterios, como el de máxima relación retorno-riesgo (medido por el ratio de Sharpe) y el de menor varianza posible. Este ejercicio no solo ilustra la aplicabilidad práctica del modelo de Markowitz, sino que además sirve de punto de partida para reflexiones más profundas sobre las decisiones reales de inversión, incorporando tanto fundamentos cuantitativos como elementos provenientes de las finanzas comportamentales.

2. Pregunta de Investigación

¿Cómo varía la composición y desempeño de los portafolios eficientes construidos bajo el modelo de media-varianza de Markowitz cuando se aplican criterios de optimización orientados a la maximización del ratio de Sharpe y a la minimización de la varianza, en comparación con el comportamiento empírico de los activos en el periodo 2020–2025?

3. Definiciones Teóricas

Supuestos del Modelo

  1. Los inversionistas son racionales y aversos al riesgo.
  2. El riesgo se mide como la varianza de los retornos.
  3. Las decisiones se basan en el retorno esperado y la varianza.
  4. Los retornos de los activos siguen una distribución normal.
  5. No existen costos de transacción ni impuestos.
  6. Los activos son perfectamente divisibles y líquidos.

Notación del modelo

\(n\): Número de activos
\(w_i\): Proporción del portafolio invertida en el activo \(i\)
\(\mu_i\): Retorno esperado del activo \(i\)
\(\mu_p\): Retorno esperado del portafolio
\(\sigma_p^2\): Varianza del portafolio
\(\Sigma\): Matriz de covarianzas de dimensión \(n \times n\)
\(\mu\): Vector de retornos esperados \(n \times 1\)
\(w\): Vector de pesos del portafolio \(n \times 1\)

Objetivo del modelo

Minimizar el riesgo (varianza) para un retorno esperado dado \(\mu_p\):

\[ \min_w \quad \sigma_p^2 = w^\top \Sigma w \]

Sujeto a:

\[ w^\top \mu = \mu_p \quad \text{(Retorno esperado)} \]

\[ w^\top \mathbf{1} = 1 \quad \text{(100% del capital invertido)} \]

\[ w_i \geq 0 \quad \text{(opcional: sin ventas en corto)} \]

Cálculo del retorno y riesgo del portafolio

Retorno esperado del portafolio:

\[ \mu_p = \sum_{i=1}^{n} w_i \mu_i = w^\top \mu \]

Varianza del portafolio:

\[ \sigma_p^2 = \sum_{i=1}^{n} \sum_{j=1}^{n} w_i w_j \sigma_{ij} = w^\top \Sigma w \]

donde \(\sigma_{ij} = \text{Cov}(r_i, r_j)\)

Método de Lagrange

Minimizar:

\[ w^\top \Sigma w \]

Sujeto a:

\[ w^\top \mu = \mu_p \quad \text{y} \quad w^\top \mathbf{1} = 1 \]

Función Lagrangiana:

\[ \mathcal{L}(w, \lambda_1, \lambda_2) = w^\top \Sigma w - \lambda_1 (w^\top \mu - \mu_p) - \lambda_2(w^\top \mathbf{1} - 1) \]

Condición de primer orden:

\[ \nabla_w \mathcal{L} = 2\Sigma w - \lambda_1 \mu - \lambda_2 \mathbf{1} = 0 \]

Entonces:

\[ \Sigma w = \frac{1}{2} (\lambda_1 \mu + \lambda_2 \mathbf{1}) \]

Frontera eficiente

La frontera eficiente representa los portafolios con:

  • Mínimo riesgo para un retorno esperado dado.
  • Máximo retorno para un nivel de riesgo dado.

Su forma es una parábola en el espacio retorno-riesgo:

\[ \sigma_p^2(\mu_p) = a\mu_p^2 - 2b\mu_p + c \]

Los parámetros \(a, b, c\) se calculan en función de \(\mu\) y \(\sigma\).

Portafolio de mínima varianza

Es el portafolio con el riesgo más bajo posible, sin importar el retorno esperado:

\[ w_{\text{GMVP}} = \frac{\Sigma^{-1} \mathbf{1}}{\mathbf{1}^\top \Sigma^{-1} \mathbf{1}} \]

Portafolio tangente con activo libre de riesgo

Si existe un activo libre de riesgo con retorno \(r_f\), el portafolio óptimo (máxima pendiente de la línea de mercado) se obtiene como:

\[ w_T = \frac{\Sigma^{-1} (\mu - r_f \mathbf{1})}{\mathbf{1}^\top \Sigma^{-1} (\mu - r_f \mathbf{1})} \]

Sharpe Ratio del portafolio tangente:

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

Visualización típica

  • Eje horizontal: Riesgo \(\sigma\)
  • Eje vertical: Retorno esperado \(\mu\)
  • La frontera eficiente es la parte superior de la curva.
  • El portafolio tangente es donde la línea de mercado toca la frontera.

4. Estadisticas Descriptivas y Análisis

# Cargar librerías
library(quantmod)
library(PerformanceAnalytics)
library(tidyquant)
library(tidyverse)
library(gganimate)
library(ggplot2)
library(gifski)
library(magick)

Definiendo y descargando precios de acciones

# Definir los activos
assets <- c("META", "AMZN", "AAPL", "NFLX", "GOOG", "TSLA")
n_assets <- length(assets)

# Descargar precios ajustados desde Yahoo Finance usando tidyquant
prices_df <- tq_get(assets,
                    from = "2020-01-01",
                    to = "2025-07-07",
                    get = "stock.prices")

# Ver los primeros registros
head(prices_df)
## # A tibble: 6 × 8
##   symbol date        open  high   low close   volume adjusted
##   <chr>  <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl>
## 1 META   2020-01-02  207.  210.  206.  210. 12077100     209.
## 2 META   2020-01-03  207.  210.  207.  209. 11188400     208.
## 3 META   2020-01-06  207.  213.  207.  213. 17058900     211.
## 4 META   2020-01-07  213.  215.  212.  213. 14912400     212.
## 5 META   2020-01-08  213   216.  213.  215. 13475000     214.
## 6 META   2020-01-09  218.  218.  216.  218. 12642800     217.
# Mostrar las últimas filas del dataframe
tail(prices_df)
## # A tibble: 6 × 8
##   symbol date        open  high   low close    volume adjusted
##   <chr>  <date>     <dbl> <dbl> <dbl> <dbl>     <dbl>    <dbl>
## 1 TSLA   2025-06-26  325.  331.  324.  326.  80440900     326.
## 2 TSLA   2025-06-27  325.  329.  318.  324.  89067000     324.
## 3 TSLA   2025-06-30  320.  326.  317.  318.  76695100     318.
## 4 TSLA   2025-07-01  298.  306.  293.  301. 145085700     301.
## 5 TSLA   2025-07-02  313.  317.  304.  316. 119483700     316.
## 6 TSLA   2025-07-03  318.  318.  313.  315.  58042300     315.

Calculando retornos

# Calcular los retornos diarios porcentuales a partir de precios ajustados
returns <- prices_df %>%
  select(date, symbol, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date) %>%
  mutate(across(-date, ~ (. - lag(.)) / lag(.))) %>%
  drop_na()

# Mostrar primeras filas
head(returns)
## # A tibble: 6 × 7
##   date           META     AMZN     AAPL    NFLX      GOOG     TSLA
##   <date>        <dbl>    <dbl>    <dbl>   <dbl>     <dbl>    <dbl>
## 1 2020-01-03 -0.00529 -0.0121  -0.00972 -0.0119 -0.00491   0.0296 
## 2 2020-01-06  0.0188   0.0149   0.00797  0.0305  0.0247    0.0193 
## 3 2020-01-07  0.00216  0.00209 -0.00470 -0.0151 -0.000624  0.0388 
## 4 2020-01-08  0.0101  -0.00781  0.0161   0.0257  0.00788   0.0492 
## 5 2020-01-09  0.0143   0.00480  0.0212  -0.0106  0.0110   -0.0219 
## 6 2020-01-10 -0.00110 -0.00941  0.00226 -0.0197  0.00697  -0.00663

Graficando los retornos

library(ggplot2)
library(patchwork)  # Para combinar plots fácilmente

# Convertir a formato largo para ggplot
returns_long <- returns %>%
  pivot_longer(-date, names_to = "symbol", values_to = "retorno")

# Crear un gráfico para cada activo
plots <- returns_long %>%
  split(.$symbol) %>%
  map(~ ggplot(data = .x, aes(x = date, y = retorno)) +
        geom_line(color = "#2c3e50", size = 0.6) +
        geom_smooth(method = "loess", se = FALSE, color = "#e74c3c", linetype = "dashed") +
        labs(title = paste("Retornos diarios de", unique(.x$symbol)),
             x = "Fecha", y = "Retorno") +
        theme_minimal(base_size = 12) +
        theme(plot.title = element_text(face = "bold", color = "#34495e")))

# Combinar en una cuadrícula 2x2 (ajustar si hay más de 4 activos)
wrap_plots(plots, ncol = 2)

Visualización de Retornos

library(lubridate)
library(ggplot2)

# Asegúrate de tener los retornos en formato largo
returns_long <- returns %>%
  pivot_longer(-date, names_to = "symbol", values_to = "retorno")

# Agregar columnas de día, mes y año
returns_long <- returns_long %>%
  mutate(
    dia_semana = wday(date, label = TRUE, abbr = FALSE, week_start = 1),  # Lunes a domingo
    mes = month(date, label = TRUE, abbr = FALSE),
    anio = year(date)
  )

# Boxplot por día de la semana
ggplot(returns_long, aes(x = dia_semana, y = retorno, fill = symbol)) +
  geom_boxplot(alpha = 0.6, outlier.size = 0.5) +
  facet_wrap(~symbol) +
  labs(title = "Distribución de retornos por día de la semana", x = "Día", y = "Retorno") +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

El gráfico muestra la distribución de los retornos diarios por día de la semana para seis activos tecnológicos (AAPL, AMZN, GOOG, META, NFLX y TSLA). En general, los retornos están centrados cerca de cero, lo que indica ausencia de un sesgo sistemático y sugiere que no hay un efecto del día de la semana claramente aprovechable. La mayoría de los activos presenta una dispersión similar entre lunes y viernes, aunque TSLA y NFLX destacan por su mayor volatilidad, evidenciada en la amplitud de sus boxplots y la presencia frecuente de valores atípicos. Esta mayor variabilidad sugiere que son activos más sensibles a eventos externos o noticias. A pesar de leves asimetrías en algunos casos, como colas negativas más extendidas en TSLA, el análisis visual respalda la idea de que los retornos diarios no muestran patrones consistentes por día de la semana.

# Boxplot por mes
ggplot(returns_long, aes(x = mes, y = retorno, fill = symbol)) +
  geom_boxplot(alpha = 0.6, outlier.size = 0.5) +
  facet_wrap(~symbol) +
  labs(title = "Distribución de retornos por mes", x = "Mes", y = "Retorno") +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

El gráfico permite observar cómo varía la distribución de los retornos financieros de seis empresas tecnológicas a lo largo de los meses del año. Aunque las medianas de los retornos se mantienen generalmente cercanas a cero lo cual indica que, en promedio, no hay un sesgo mensual fuerte, existen diferencias en la dispersión y simetría de las distribuciones. TSLA, por ejemplo, muestra una mayor amplitud en sus cajas y bigotes en casi todos los meses, lo que refleja una mayor sensibilidad a eventos del mercado o a condiciones específicas de la empresa. Por otro lado, acciones como AAPL y GOOG exhiben distribuciones más concentradas, lo que puede interpretarse como mayor estabilidad en sus retornos. La recurrencia de outliers en todos los activos sugiere que, independientemente del mes, los retornos financieros están expuestos a eventos extremos, lo cual es típico en instrumentos de renta variable. Más que mostrar una estacionalidad marcada, la gráfica destaca diferencias estructurales entre las acciones, revelando perfiles de riesgo distintos que podrían ser relevantes para decisiones de inversión o construcción de portafolios diversificados.

# Boxplot por año
ggplot(returns_long, aes(x = as.factor(anio), y = retorno, fill = symbol)) +
  geom_boxplot(alpha = 0.6, outlier.size = 0.5) +
  facet_wrap(~symbol) +
  labs(title = "Distribución de retornos por año", x = "Año", y = "Retorno") +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

La gráfica revela diferencias claras en la variabilidad de los retornos entre las acciones analizadas a lo largo de varios años. AAPL, GOOG y AMZN muestran una distribución de retornos más estable y concentrada en torno a cero, lo que sugiere un comportamiento predecible y menor riesgo relativo. En contraste, TSLA se destaca por presentar una alta dispersión en todos los años, evidenciando una volatilidad estructural significativamente mayor. La presencia consistente de valores atípicos en todas las acciones refleja la naturaleza impredecible de los mercados financieros, aunque en algunos años como 2020 y 2022 estos extremos son más pronunciados, posiblemente debido a eventos macroeconómicos. Aunque la mayoría de las distribuciones son simétricas, en algunos años se observan ligeros sesgos que indican periodos con retornos positivos o negativos predominantes. En conjunto, esta visualización resalta perfiles de riesgo diferenciados entre las empresas, útiles para la toma de decisiones en contextos de inversión y gestión de portafolios.

Definiendo los pesos del portafolio

# Número de activos
n_assets <- length(assets)

# Calcular pesos iguales para cada activo
portfolio_weights <- rep(1 / n_assets, n_assets)

# Mostrar los pesos
portfolio_weights
## [1] 0.1666667 0.1666667 0.1666667 0.1666667 0.1666667 0.1666667
# Asegúrate de tener los datos en formato xts o matriz
# returns ya debería estar en formato ancho con columnas: date, AAPL, AMZN, etc.

# Eliminar columna 'date' y convertir a matriz
returns_matrix <- as.matrix(returns[,-1])

# Calcular retornos del portafolio: producto escalar de los pesos con las columnas de retornos
portfolio_returns <- returns_matrix %*% portfolio_weights

# Convertir a serie con fechas
portfolio_returns <- tibble(
  date = returns$date,
  retorno_portafolio = as.vector(portfolio_returns)
)

# Mostrar las primeras filas
head(portfolio_returns)
## # A tibble: 6 × 2
##   date       retorno_portafolio
##   <date>                  <dbl>
## 1 2020-01-03           -0.00238
## 2 2020-01-06            0.0193 
## 3 2020-01-07            0.00377
## 4 2020-01-08            0.0169 
## 5 2020-01-09            0.00314
## 6 2020-01-10           -0.00460
library(PerformanceAnalytics)
library(xts)

# Convertir retornos del portafolio a objeto xts
portfolio_xts <- xts(portfolio_returns$retorno_portafolio, order.by = portfolio_returns$date)
colnames(portfolio_xts) <- "1/n Portafolio"

# Gráfico de rendimiento acumulado, drawdowns y retornos
charts.PerformanceSummary(portfolio_xts,
                          main = "1/n Rendimiento del portafolio",
                          colorset = rich8equal, 
                          geometric = TRUE)

El plot generado muestra los rendimientos acumulados del portafolio, el gráfico inferior (medio) representa los rendimientos diarios de todo el portafolio.

Encontrando la frontera eficiente

# Número de portafolios a simular
N_PORTFOLIOS <- 10^6

# Número de días hábiles en un año (para anualizar retornos)
N_DAYS <- 252

# Definir activos
assets <- c("META", "AMZN", "AAPL", "NFLX", "GOOG", "TSLA")

# Ordenar alfabéticamente
assets <- sort(assets)

# Número de activos
n_assets <- length(assets)

# Mostrar resultado
assets
## [1] "AAPL" "AMZN" "GOOG" "META" "NFLX" "TSLA"
n_assets
## [1] 6

Descargando activos

library(tidyquant)

# Ya deberías tener definidos los activos en 'assets', ordenados alfabéticamente

# Descargar precios diarios ajustados de Yahoo Finance
prices_df <- tq_get(assets,
                    from = "2021-01-01",
                    to = "2025-07-07",
                    get = "stock.prices")

# Vista previa
head(prices_df)
## # A tibble: 6 × 8
##   symbol date        open  high   low close    volume adjusted
##   <chr>  <date>     <dbl> <dbl> <dbl> <dbl>     <dbl>    <dbl>
## 1 AAPL   2021-01-04  134.  134.  127.  129. 143301900     126.
## 2 AAPL   2021-01-05  129.  132.  128.  131.  97664900     128.
## 3 AAPL   2021-01-06  128.  131.  126.  127. 155088000     123.
## 4 AAPL   2021-01-07  128.  132.  128.  131. 109578200     128.
## 5 AAPL   2021-01-08  132.  133.  130.  132. 105158200     129.
## 6 AAPL   2021-01-11  129.  130.  128.  129. 100384500     126.

Calculando retornos anuales y su desviación estándar

library(dplyr)
library(tidyr)
library(PerformanceAnalytics)

# Usamos los precios ajustados y convertimos a formato ancho
returns_df <- prices_df %>%
  select(date, symbol, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date) %>%
  mutate(across(-date, ~ (. / lag(.) - 1))) %>%
  drop_na()

# Eliminar la columna de fecha para cálculos matriciales
returns_mat <- as.matrix(returns_df[, -1])

# Constante de días para anualizar
N_DAYS <- 252

# Calcular retornos promedio anualizados (media * N_DAYS)
avg_returns <- colMeans(returns_mat) * N_DAYS

# Calcular matriz de covarianza anualizada
cov_mat <- cov(returns_mat) * N_DAYS

# Mostrar resultados
avg_returns
##      AAPL      AMZN      GOOG      META      NFLX      TSLA 
## 0.1577279 0.1386332 0.2146874 0.3208894 0.3059226 0.2494252
cov_mat
##            AAPL       AMZN       GOOG       META       NFLX       TSLA
## AAPL 0.08116578 0.05864788 0.05281375 0.06471880 0.05311366 0.09074834
## AMZN 0.05864788 0.12655115 0.07241290 0.09778774 0.07876057 0.10136037
## GOOG 0.05281375 0.07241290 0.09773094 0.08304984 0.05830386 0.08113383
## META 0.06471880 0.09778774 0.08304984 0.19786274 0.09457012 0.09801674
## NFLX 0.05311366 0.07876057 0.05830386 0.09457012 0.19782693 0.09830896
## TSLA 0.09074834 0.10136037 0.08113383 0.09801674 0.09830896 0.38608202

Simulando pesos aleatorios

set.seed(42)  # Para reproducibilidad

# Simular matriz de pesos aleatorios
weights_raw <- matrix(runif(N_PORTFOLIOS * n_assets), nrow = N_PORTFOLIOS, ncol = n_assets)

# Normalizar filas para que la suma de pesos en cada portafolio sea 1
weights <- weights_raw / rowSums(weights_raw)

# Ver estructura y ejemplo
dim(weights)  # debería ser N_PORTFOLIOS x n_assets
## [1] 1000000       6
head(weights)
##            [,1]       [,2]       [,3]       [,4]       [,5]       [,6]
## [1,] 0.30739088 0.15508958 0.03430836 0.07286651 0.19021835 0.24012631
## [2,] 0.31406338 0.13868581 0.02405097 0.16114362 0.18131000 0.18074622
## [3,] 0.08184604 0.24440838 0.14243640 0.20740734 0.20942488 0.11447696
## [4,] 0.27370299 0.05233305 0.16390355 0.13057442 0.10038064 0.27910536
## [5,] 0.21031295 0.17129578 0.22633776 0.22762969 0.08786683 0.07655699
## [6,] 0.15061600 0.11719253 0.21601262 0.21345629 0.27309102 0.02963153

Calculando métricas del portafolio

# Calcular retorno esperado del portafolio: W x mu
portf_rtns <- weights %*% avg_returns  # Vector de tamaño N_PORTFOLIOS

# Calcular volatilidad (desviación estándar) de cada portafolio
portf_vol <- apply(weights, 1, function(w) {
  sqrt(t(w) %*% cov_mat %*% w)
})

# Calcular ratio de Sharpe (sin activo libre de riesgo, rf = 0)
portf_sharpe_ratio <- as.vector(portf_rtns / portf_vol)

# Ver ejemplo de primeros resultados
head(cbind(portf_rtns, portf_vol, portf_sharpe_ratio))
##                portf_vol portf_sharpe_ratio
## [1,] 0.2188180 0.3213010          0.6810375
## [2,] 0.2261852 0.3114370          0.7262632
## [3,] 0.2365479 0.3145405          0.7520426
## [4,] 0.2278383 0.3266782          0.6974394
## [5,] 0.2245310 0.2952433          0.7604948
## [6,] 0.2458097 0.3013836          0.8156043

Guardando los resultados de la simulación

library(tibble)

# Crear un data frame con los resultados
portf_results_df <- tibble(
  returns = as.vector(portf_rtns),
  volatility = as.vector(portf_vol),
  sharpe_ratio = as.vector(portf_sharpe_ratio)
)

portf_results_df<-data.frame(portf_results_df)
# Mostrar primeras filas
head(portf_results_df)
##     returns volatility sharpe_ratio
## 1 0.2188180  0.3213010    0.6810375
## 2 0.2261852  0.3114370    0.7262632
## 3 0.2365479  0.3145405    0.7520426
## 4 0.2278383  0.3266782    0.6974394
## 5 0.2245310  0.2952433    0.7604948
## 6 0.2458097  0.3013836    0.8156043

Frontera Eficiente

library(ggplot2)
library(dplyr)

# Número de puntos a evaluar en la frontera
N_POINTS <- 200

# Redondear retornos de portafolios
portf_results_df <- portf_results_df %>%
  mutate(returns_round = round(returns, 2))

portf_results_df<-data.frame(portf_results_df)
colnames(portf_results_df)
## [1] "returns"       "volatility"    "sharpe_ratio"  "returns_round"
# Crear posibles niveles de retorno para la frontera eficiente
possible_ef_rtns <- seq(
  from = min(portf_results_df$returns_round),
  to = max(portf_results_df$returns_round),
  length.out = N_POINTS
) %>% round(2)

# Calcular la frontera eficiente: menor volatilidad por nivel de retorno
ef_df <- map_dfr(possible_ef_rtns, function(rtn) {
  subset <- filter(portf_results_df, returns_round == rtn)
  if (nrow(subset) > 0) {
    vol_min <- min(subset$volatility)
    tibble(returns = rtn, volatility = vol_min)
  } else {
    NULL
  }
})

# Crear tibble de los activos individuales
individual_assets_df <- tibble(
  asset = assets,
  return = as.vector(avg_returns),
  volatility = sqrt(diag(cov_mat))
)


library(ggplot2)
library(viridis)  # para una escala de colores profesional

ggplot() +
  # Portafolios simulados: color por Sharpe ratio
  geom_point(
    data = portf_results_df,
    mapping = aes(x = volatility, y = returns, color = sharpe_ratio),
    alpha = 0.5, size = 1.3
  ) +

  # Activos individuales
  geom_point(
    data = individual_assets_df,
    mapping = aes(x = volatility, y = return),
    shape = 23, size = 4, fill = "#2980b9", color = "black", stroke = 1
  ) +

  # Etiquetas de activos
  geom_text(
    data = individual_assets_df,
    mapping = aes(x = volatility, y = return, label = asset),
    hjust = -0.3, vjust = -0.1, size = 3.5
  ) +

  # Escala de color tipo viridis
  scale_color_viridis(name = "Sharpe Ratio", option = "C", direction = -1) +

  # Títulos y tema
  labs(
    title = "Frontera Eficiente de Portafolios Simulados",
    x = "Volatilidad Anual",
    y = "Retorno Esperado Anual"
  ) +

  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", size = 15, color = "#2c3e50"),
    plot.subtitle = element_text(size = 12, color = "#7f8c8d"),
    legend.position = "right"
  )

El gráfico muestra la frontera eficiente generada a partir de un millón de portafolios simulados construidos con combinaciones aleatorias de seis acciones tecnológicas (AAPL, AMZN, GOOG, META, NFLX y TSLA). En el eje X se representa la volatilidad anual (riesgo), y en el eje Y, el retorno esperado anual. Cada punto corresponde a un portafolio, y su color indica el índice de Sharpe: los colores más oscuros (violeta) representan valores más altos del ratio de Sharpe, lo que indica una mejor relación retorno-riesgo, mientras que los colores más claros (amarillo) indican desempeño menos eficiente. La nube de portafolios forma una parábola cóncava que ilustra la clásica relación riesgo-retorno. Aquellos portafolios ubicados sobre la parte superior de la curva conforman la frontera eficiente. Se observa que activos como AAPL y META están relativamente cerca de esta frontera, mientras que TSLA se ubica más lejos y hacia la derecha, reflejando su mayor volatilidad. Este análisis permite identificar visualmente combinaciones de activos que ofrecen el mejor rendimiento ajustado por riesgo, así como evaluar el comportamiento relativo de cada acción dentro del conjunto simulado.

Identificando el portafolio de con el Máximo Ratio de Sharpe y Mínima Varianza

# Portafolio con mayor Sharpe Ratio
max_sharpe_port <- portf_results_df %>%
  filter(sharpe_ratio == max(sharpe_ratio)) %>%
  slice(1)

max_sharpe_port
##     returns volatility sharpe_ratio returns_round
## 1 0.2688419  0.3136914    0.8570267          0.27
# Portafolio con mínima varianza
min_var_port <- portf_results_df %>%
  filter(volatility == min(volatility)) %>%
  slice(1)
min_var_port
##     returns volatility sharpe_ratio returns_round
## 1 0.1806792  0.2633127    0.6861773          0.18

Graficando los portafolios anteriores

library(ggplot2)
library(viridis)

ggplot() +
  # Portafolios simulados
  geom_point(data = portf_results_df,
             aes(x = volatility, y = returns, color = sharpe_ratio),
             alpha = 0.5, size = 1.2) +

  # Activos individuales
  geom_point(data = individual_assets_df,
             aes(x = volatility, y = return),
             shape = 23, size = 4, fill = "#2980b9", color = "black") +
  geom_text(data = individual_assets_df,
            aes(x = volatility, y = return, label = asset),
            hjust = -0.3, vjust = -0.2, size = 3.5) +

  # Portafolio de máxima Sharpe
  geom_point(data = max_sharpe_port,
             aes(x = volatility, y = returns),
             color = "gold", shape = 17, size = 4) +
  geom_text(data = max_sharpe_port,
            aes(x = volatility, y = returns, label = "Máx. Sharpe"),
            hjust = -0.2, vjust = 1.5, color = "gold", size = 3.5) +

  # Portafolio de mínima varianza
  geom_point(data = min_var_port,
             aes(x = volatility, y = returns),
             color = "midnightblue", shape = 15, size = 4) +
  geom_text(data = min_var_port,
            aes(x = volatility, y = returns, label = "Mín. Varianza"),
            hjust = -0.2, vjust = 1.5, color = "midnightblue", size = 3.5) +

  # Escala de color
  scale_color_viridis(name = "Sharpe Ratio", option = "C", direction = -1) +

  labs(
    title = "Frontera Eficiente con Portafolios Óptimos Destacados",
    subtitle = "Máx. Sharpe (dorado) y Mín. Varianza (azul oscuro)",
    x = "Volatilidad Anual",
    y = "Retorno Esperado Anual"
  ) +

  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))

En el gráfico se visualizan dos portafolios clave dentro del universo simulado: el de máximo Sharpe Ratio, marcado con un triángulo amarillo, y el de mínima varianza, señalado con un cuadrado negro. El portafolio con mayor Sharpe Ratio representa la mejor combinación riesgo-retorno posible, ya que ofrece el mayor retorno esperado ajustado por unidad de riesgo; se ubica en la parte superior de la nube eficiente, donde los valores del Sharpe son más altos (zona púrpura), con una volatilidad moderada y un rendimiento elevado. Por otro lado, el portafolio de mínima varianza es el más seguro dentro del conjunto, con la menor volatilidad entre todas las combinaciones posibles, aunque con un retorno esperado también más bajo. Este equilibrio permite a los inversores elegir entre eficiencia (máximo Sharpe) o seguridad (mínima varianza), según su perfil de riesgo. Ambos portafolios actúan como referencias fundamentales en la construcción de carteras óptimas bajo el enfoque de teoría moderna de portafolios.

Encontrando las ponderaciones de los portafolios óptimos

# Índices de los portafolios óptimos
idx_max_sharpe <- which.max(portf_results_df$sharpe_ratio)
idx_min_var <- which.min(portf_results_df$volatility)

# Nombres de los activos
activos <- colnames(returns_df)
activos <- activos[-1]

# Pesos de los portafolios óptimos
pesos_max_sharpe <- weights[idx_max_sharpe, ]
pesos_min_var <- weights[idx_min_var, ]

# Crear dataframes
tabla_pesos <- data.frame(
  Activo = activos,
  `Máx. Sharpe` = round(pesos_max_sharpe, 4),
  `Mín. Varianza` = round(pesos_min_var, 4)
)

# Mostrar tabla
knitr::kable(tabla_pesos, caption = "Pesos por Activo en los Portafolios Óptimos")
Pesos por Activo en los Portafolios Óptimos
Activo Máx..Sharpe Mín..Varianza
AAPL 0.0622 0.4998
AMZN 0.0035 0.1213
GOOG 0.3403 0.3366
META 0.2714 0.0027
NFLX 0.3174 0.0358
TSLA 0.0053 0.0037

La tabla muestra la asignación de pesos para cada uno de los seis activos (AAPL, AMZN, GOOG, META, NFLX y TSLA) dentro de dos portafolios construidos bajo criterios de eficiencia: el primero busca maximizar el Sharpe Ratio, y el segundo busca minimizar la varianza. Estas estrategias responden a objetivos distintos: el portafolio de máximo Sharpe Ratio busca obtener el mayor rendimiento ajustado por riesgo, mientras que el de mínima varianza privilegia la estabilidad, asignando los recursos para minimizar la volatilidad total del portafolio, sin considerar directamente el retorno.

En el caso del portafolio de Sharpe máximo, se observa una fuerte concentración en GOOG (34.03%), NFLX (31.74%) y META (27.14%), lo cual sugiere que estos activos han mostrado un buen equilibrio entre rendimiento y riesgo durante el periodo analizado. AAPL, a pesar de ser una empresa sólida, solo representa un 6.22% del portafolio, lo que indica que, aunque tiene buen rendimiento, su inclusión no maximiza la eficiencia total. Por su parte, AMZN (0.35%) y TSLA (0.53%) tienen participaciones marginales, probablemente debido a una menor relación retorno-riesgo o mayor volatilidad relativa.

En contraste, el portafolio de mínima varianza está altamente concentrado en AAPL (49.98%) y GOOG (33.66%), activos que aparentemente presentan baja correlación con otros activos y una volatilidad reducida. Este portafolio distribuye muy poco capital en META (0.27%), NFLX (3.58%), TSLA (0.37%) y AMZN (12.13%), reflejando una estrategia defensiva que favorece empresas con rendimientos más estables y menos sensibles a choques del mercado.

En conjunto, la tabla ilustra cómo varía la composición de un portafolio dependiendo del criterio de optimización adoptado. Mientras que el portafolio eficiente en términos de Sharpe diversifica más el capital entre activos con buen desempeño ajustado por riesgo, el portafolio de mínima varianza concentra el capital en unos pocos activos con menor volatilidad. Esta información es crucial para que un inversionista pueda tomar decisiones alineadas con su perfil: quienes prioricen el crecimiento ajustado por riesgo pueden inclinarse por el portafolio de Sharpe máximo, mientras que los más conservadores podrían preferir el de menor varianza.

5. Conclusiones

El análisis realizado se desarrolló bajo el marco de la teoría moderna de portafolio de Markowitz, la cual plantea que los inversionistas racionales deben tomar decisiones basadas en la optimización del retorno esperado frente al riesgo, medido a través de la varianza de los retornos. A través de la simulación de un millón de portafolios utilizando datos históricos de seis activos tecnológicos relevantes (AAPL, AMZN, GOOG, META, NFLX y TSLA), se logró construir la frontera eficiente, una herramienta visual clave que delimita las combinaciones óptimas de portafolios en el espacio retorno-riesgo.

Entre los portafolios generados, se destacaron dos configuraciones: el portafolio con el máximo Sharpe Ratio y el portafolio de mínima varianza. El primero mostró una asignación significativa en GOOG (34.03 %), NFLX (31.74 %) y META (27.14 %), lo cual sugiere que estos activos han ofrecido una atractiva combinación de rendimiento y riesgo durante el periodo analizado. Por su parte, el portafolio de mínima varianza concentró más del 80 % del capital en AAPL (49.98 %) y GOOG (33.66 %), evidenciando que estos activos han sido los más estables dentro del conjunto considerado.

El gráfico de la frontera eficiente confirmó que el portafolio de máxima eficiencia se ubica en la parte superior izquierda de la nube de puntos, alcanzando altos retornos con una volatilidad contenida, mientras que el portafolio de mínima varianza se posiciona en el extremo izquierdo, siendo el más estable pero con menor rendimiento esperado. Estas observaciones respaldan empíricamente los principios teóricos de la optimización de portafolios.

Al considerar elementos de las finanzas comportamentales, se pone de relieve que los inversionistas no siempre actúan de manera puramente racional. Sesgos como la sobreconfianza, la aversión a las pérdidas o la tendencia a seguir al mercado pueden llevar a decisiones que se desvían de las recomendaciones del modelo de Markowitz. Por ejemplo, activos como TSLA o AMZN, que tienen fuerte presencia mediática pero no aparecen significativamente en los portafolios óptimos, podrían ser sobreponderados por razones emocionales más que técnicas. Asimismo, un inversor conservador podría priorizar el portafolio de mínima varianza no por razones de eficiencia financiera, sino por la tranquilidad emocional que brinda su baja volatilidad.

Por lo tanto, los resultados del ejercicio no solo validan la aplicabilidad del enfoque de Markowitz, sino que también muestran la necesidad de interpretar los modelos cuantitativos dentro de un contexto más amplio que incluya el comportamiento real de los agentes. La integración de ambos enfoques permite diseñar estrategias más robustas, adaptadas tanto a los objetivos financieros como a las características psicológicas del inversionista.