1. Introducción

En este trabajo aplicamos el modelo de optimización de portafolios de Markowitz para analizar cómo se pueden combinar distintos activos buscando el mejor equilibrio entre riesgo y retorno. A partir de precios históricos obtenidos desde Yahoo Finance, construimos la frontera eficiente y los portafolios óptimos, como el de máxima relación retorno-riesgo y el de mínima varianza.

Además de implementar el modelo base, realizamos una serie de modificaciones para entender cómo cambian los resultados: se reemplazan los activos del portafolio, se calculan los retornos en logaritmos, se incrementa el número de simulaciones y se cambia la semilla aleatoria. Con esto buscamos observar cómo cada ajuste afecta la forma de la frontera eficiente, los pesos de los activos y la estabilidad de los resultados del modelo.

Para poner en práctica este marco teórico, se trabajó con precios históricos diarios de un conjunto diversificado de activos pertenecientes a los sectores salud, semiconductores, software, consumo e industrial. Esta selección se realizó con el fin de capturar el comportamiento de distintos segmentos del mercado y mejorar la representatividad del análisis. Los datos fueron extraídos directamente desde la plataforma Yahoo Finance e incluyen los siguientes activos: LLY (Eli Lilly and Company) y JNJ (Johnson & Johnson) para el sector salud; AMD para semiconductores; ADBE (Adobe) para software; KHC (Kraft Heinz) para consumo; y CAT (Caterpillar) para el sector industrial. 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 13 de noviembre 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 forma de la frontera eficiente, la relación riesgo–retorno y la composición de los portafolios óptimos en el modelo de Markowitz al modificar los activos seleccionados, el tipo de retorno utilizado, el número de simulaciones y la semilla aleatoria empleada en la generación de escenarios?

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("LLY", "JNJ", "AMD", "ADBE", "KHC", "CAT")
n_assets <- length(assets)

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

# Ver los primeros registros
head(prices_df)
# Mostrar las últimas filas del dataframe
tail(prices_df)

Calculando retornos

#Rendimientos logaritmicos (vamos a trabajar con estos)

returns_log<-prices_df %>%
  select(date,symbol,adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date) %>%
  mutate(across(-date, ~log (.) -log(lag(.)))) %>%
  drop_na()

head(returns_log)
# Calcular los retornos diarios porcentuales a partir de precios ajustados
returns_aj <- 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_aj)

En el desarrollo del trabajo, el principal ajuste metodológico consistió en utilizar rendimientos continuamente compuestos en lugar de rendimientos simples. Esta decisión se fundamenta en que los rendimientos calculados a través del logaritmo presentan propiedades estadísticas más adecuadas para el análisis de riesgo. A diferencia de los precios, que suelen mostrar tendencias alcistas, bajistas o laterales y, por tanto, no son estacionarios, los rendimientos continuos tienden a fluctuar alrededor de una media cercana a cero y mantienen una varianza relativamente constante en el tiempo. Esta característica de estacionariedad, entendida como la constancia de los dos primeros momentos de la distribución (media y varianza), es esencial para la modelación y pronóstico. Además, los rendimientos compuestos son aditivos, lo que permite trabajar de manera más robusta con series largas y reduce la sensibilidad a datos atípicos. Por estas razones, el uso de rendimientos continuos ofrece una representación más estable y estadísticamente apropiada del comportamiento del riesgo financiero.

Graficando los retornos

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

# Convertir a formato largo para ggplot
returns_long <- returns_log %>%
  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.4) +
        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 2x3 (ajustar si hay más de 4 activos)
wrap_plots(plots, nrow = 3, ncol=2)

Visualización de Retornos

library(lubridate)
library(ggplot2)

# Asegúrate de tener los retornos en formato largo
returns_long <- returns_log %>%
  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 las seis acciones analizadas (ADBE, AMD, CAT, JNJ, KHC y LLY). En general, los retornos se concentran cerca de cero, lo que indica la ausencia de un patrón sistemático o efecto del día de la semana. La dispersión es similar entre lunes y viernes, aunque AMD presenta una volatilidad ligeramente mayor, visible en la mayor amplitud de sus cajas y la presencia de valores atípicos. En contraste, empresas como JNJ y KHC muestran retornos más estables y menos dispersos. Esto sugiere que, dentro del grupo, los activos tecnológicos tienden a ser algo más volátiles que los de sectores defensivos como salud o consumo básico.

# 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 mensual permite observar cómo varían los retornos a lo largo del año. En la mayoría de los activos, las medianas se mantienen cerca de cero, lo que indica que no hay una estacionalidad marcada en los rendimientos. AMD y ADBE exhiben una mayor variabilidad mes a mes, reflejando su naturaleza más sensible a condiciones del mercado y noticias del sector tecnológico. Por su parte, JNJ y KHC mantienen distribuciones más concentradas y simétricas, lo que denota un comportamiento más estable. En conjunto, los resultados evidencian que los activos de sectores defensivos tienden a mantener una volatilidad menor a lo largo del año, mientras que los activos de crecimiento presentan fluctuaciones más amplias.

# 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 por año muestra diferencias claras en la volatilidad de los retornos a lo largo del periodo 2020–2025. AMD y ADBE destacan por su alta dispersión en comparación con los demás activos, lo que refleja un mayor nivel de riesgo y exposición a variaciones de mercado. En contraste, JNJ, KHC y LLY presentan cajas más compactas y menos valores extremos, evidenciando estabilidad en sus rendimientos anuales. Se observan años con mayor volatilidad general, como 2020 y 2022, probablemente influenciados por factores macroeconómicos globales. En conjunto, la comparación anual resalta la diferencia de perfiles entre activos defensivos y de crecimiento, aportando una visión más completa del riesgo dentro del portafolio.

Definiendo los pesos del portafolio

En este caso, se crea un portafolio equiponderado, donde cada activo tiene el mismo peso, independientemente de su precio, volatilidad o importancia en el mercado.

# 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

Calcular retornos del portafolio

# 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_log[,-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_log$date,
  retorno_portafolio = as.vector(portfolio_returns)
)

# Mostrar las primeras filas
head(portfolio_returns)
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) <- "Portafolio"

# Gráfico de rendimiento acumulado, drawdowns y retornos
charts.PerformanceSummary(portfolio_xts,
                          main = "Rendimiento del portafolio con pesos iguales",
                          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

Antes

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

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

# Definir activos
assets_base <- c("LLY", "JNJ", "AMD", "ADBE", "KHC", "CAT")

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


# Número de activos
n_assets_base <- length(assets_base)

# Mostrar resultado
assets_base
## [1] "ADBE" "AMD"  "CAT"  "JNJ"  "KHC"  "LLY"
n_assets_base
## [1] 6

Después

Tanto en la versión anterior como en la posterior se mantienen los mismos activos y la misma estructura general; sin embargo, la diferencia fundamental entre ambas radica en la magnitud de la simulación. Inicialmente, el modelo generaba 1 millón de portafolios (10⁶), mientras que en la versión actual se incrementa significativamente la robustez del análisis al simular 10 millones de portafolios (10⁷). Este aumento sustancial en el número de simulaciones permite explorar un espacio más amplio de combinaciones posibles de pesos, lo que mejora la estimación de la frontera eficiente y reduce el error asociado al muestreo aleatorio. En esencia, la lógica del código es la misma, pero la escala de la simulación es mucho mayor, lo que conduce a resultados más precisos y representativos del universo de portafolios posibles.

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

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

# Definir activos
assets <- c("LLY", "JNJ", "AMD", "ADBE", "KHC", "CAT")

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

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

# Mostrar resultado
assets
## [1] "ADBE" "AMD"  "CAT"  "JNJ"  "KHC"  "LLY"
n_assets
## [1] 6

Descargando activos

Calculando retornos anuales y su desviación estándar

Antes

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


# Descargar precios ajustados desde Yahoo Finance usando tidyquant
prices_df_base <- tq_get(assets_base,
                    from = "2020-01-01",
                    to = "2025-11-13",
                    get = "stock.prices")

# Usamos los precios ajustados y convertimos a formato ancho
returns_df_base <- prices_df_base %>%
  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_base <- as.matrix(returns_df_base[, -1])

# Calcular retornos promedio anualizados (media * N_DAYS)
avg_returns_base <- colMeans(returns_mat_base) * N_DAYS_base

# Calcular matriz de covarianza anualizada
cov_mat_base <- cov(returns_mat_base) * N_DAYS_base

avg_returns_base
##       ADBE        AMD        CAT        JNJ        KHC        LLY 
## 0.07374213 0.42869878 0.30279520 0.09616660 0.04057021 0.41902523
cov_mat_base
##            ADBE        AMD        CAT        JNJ        KHC        LLY
## ADBE 0.14332873 0.10642096 0.03377397 0.01666798 0.02354586 0.03400188
## AMD  0.10642096 0.29188737 0.05828517 0.01175550 0.01987104 0.04060208
## CAT  0.03377397 0.05828517 0.10564527 0.01915679 0.02804392 0.02392476
## JNJ  0.01666798 0.01175550 0.01915679 0.03888006 0.02452186 0.02732926
## KHC  0.02354586 0.01987104 0.02804392 0.02452186 0.07272294 0.02339291
## LLY  0.03400188 0.04060208 0.02392476 0.02732926 0.02339291 0.11683085

Después

En este bloque de código transformamos los precios históricos ajustados de los activos en rendimientos logarítmicos, que son la base del análisis cuantitativo de portafolios debido a que son estacionarios. Luego, on esta matriz de rendimientos, calculamos el retorno promedio anualizado multiplicando la media diaria por 252, el número estándar de días hábiles en un año. Del mismo modo, se obtiene la matriz de covarianza anualizada, que representa cómo se mueven conjuntamente los activos y es un insumo esencial para medir riesgo de portafolio según la teoría de Markowitz. La matriz de covarianza captura la interrelación entre volatilidades individuales y correlaciones, mientras que los retornos promedio permiten estimar la rentabilidad esperada. En conjunto, estos elementos constituyen la base para construir la frontera eficiente y evaluar combinaciones óptimas de portafolios.

library(dplyr)
library(tidyr)
library(tidyquant)
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, ~log (.) -log(lag(.)))) %>% #aquí cambiamos para hacer rendimientos logaritmicos
  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
##         LLY         JNJ         AMD        ADBE         KHC         CAT 
## 0.361030644 0.076761950 0.284234170 0.001334144 0.004322388 0.249865057
cov_mat
##             LLY        JNJ        AMD       ADBE        KHC        CAT
## LLY  0.11493303 0.02709092 0.04061518 0.03384528 0.02313038 0.02401882
## JNJ  0.02709092 0.03878271 0.01166789 0.01658237 0.02431713 0.01915151
## AMD  0.04061518 0.01166789 0.28743694 0.10642639 0.02007644 0.05847874
## ADBE 0.03384528 0.01658237 0.10642639 0.14586477 0.02324662 0.03417985
## KHC  0.02313038 0.02431713 0.02007644 0.02324662 0.07251974 0.02802341
## CAT  0.02401882 0.01915151 0.05847874 0.03417985 0.02802341 0.10570533

Simulando pesos aleatorios

Antes

set.seed(42)  # semilla original del taller

# Matriz de pesos aleatorios
weights_raw_base <- matrix(
  runif(N_PORTFOLIOS_base * n_assets_base),
  nrow = N_PORTFOLIOS_base,
  ncol = n_assets_base
)

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

# Ver estructura y ejemplo
dim(weights_base)
## [1] 1000000       6
head(weights_base)
##            [,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

Después

En este bloque de código generamos la matriz de pesos aleatorios utilizada para simular millones de portafolios mediante el método de Monte Carlo, con el fin de aproximar la frontera eficiente a partir de múltiples combinaciones posibles de asignación de capital. Primero, se fija una semilla (set.seed(30)) para garantizar la reproducibilidad de los resultados, lo que significa que cualquier persona que ejecute el mismo código obtendrá exactamente la misma secuencia de números aleatorios y, por tanto, los mismos portafolios simulados, permitiendo validar y replicar el análisis. Aunque el ejemplo del profesor utilizaba la semilla 42, cambiarla a 30 no afecta la metodología ni los resultados globales, sino únicamente la secuencia específica de números generados. Posteriormente, se crean pesos iniciales aleatorios con distribución uniforme y se normalizan para asegurar que cada portafolio cumpla la regla fundamental de la teoría de portafolios: que la suma de sus pesos sea igual a uno y represente proporciones válidas de inversión. Este proceso permite generar un conjunto amplio y diverso de configuraciones de portafolios, lo cual es esencial para evaluar sus riesgos y retornos y construir una estimación robusta de la frontera eficiente.

set.seed(30)  # 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] 10000000        6
head(weights)
##            [,1]       [,2]       [,3]        [,4]        [,5]       [,6]
## [1,] 0.03963616 0.05261648 0.39043019 0.007209284 0.203771656 0.30633623
## [2,] 0.14389677 0.22818444 0.03090130 0.120251477 0.231589513 0.24517649
## [3,] 0.10211929 0.25458381 0.20961205 0.117063912 0.236810008 0.07981093
## [4,] 0.17195228 0.13388096 0.24815218 0.110258247 0.001716659 0.33403968
## [5,] 0.12409927 0.14700725 0.36654599 0.276797362 0.004819381 0.08073075
## [6,] 0.05992352 0.39698773 0.05210607 0.068013332 0.070194237 0.35277511

Calculando métricas del portafolio

Antes

# Calcular retorno esperado del portafolio: W x mu

portf_rtns_base <- weights_base %*% avg_returns_base

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

# Calcular ratio de Sharpe (sin activo libre de riesgo, rf = 0)
portf_sharpe_ratio_base <- as.vector(portf_rtns_base / portf_vol_base)


# Ver ejemplo de primeros resultados
head(cbind(portf_rtns_base, portf_vol_base, portf_sharpe_ratio_base))
##                portf_vol_base portf_sharpe_ratio_base
## [1,] 0.2148863      0.2436247               0.8820382
## [2,] 0.1884863      0.2326964               0.8100096
## [3,] 0.2303529      0.2273303               1.0132963
## [4,] 0.2258293      0.2215024               1.0195344
## [5,] 0.2150117      0.2264673               0.9494161
## [6,] 0.1707777      0.2069108               0.8253688

Después

En este bloque de código se evalúan las características de retorno y riesgo de cada uno de los millones de portafolios simulados. Primero, se calcula el retorno esperado de cada portafolio.Luego, se estima la volatilidad —entendida como la desviación estándar del portafolio. Finalmente, se calcula el ratio de Sharpe asumiendo una tasa libre de riesgo igual a cero, dividiendo el retorno esperado entre la volatilidad; este indicador permite comparar la eficiencia de cada portafolio en términos de rentabilidad por unidad de riesgo. Un Sharpe Ratio más alto indica una mejor eficiencia en términos riesgo-retorno, y es especialmente útil en contextos donde existen miles o millones de combinaciones posibles, pues permite identificar de manera sistemática aquellos portafolios que ofrecen el mejor desempeño ajustado al riesgo. De esta manera, el cálculo conjunto de retornos, volatilidades y Sharpe Ratios permite analizar la frontera eficiente y seleccionar portafolios óptimos según criterios de racionalidad financiera.Con este procedimiento obtenemos, para cada simulación, los valores de retorno, riesgo y desempeño relativo, los cuales son esenciales para analizar la frontera eficiente y seleccionar portafolios óptimos.

# 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.2067555 0.2872613          0.7197474
## [2,] 0.1406727 0.1944711          0.7233605
## [3,] 0.1371112 0.2159881          0.6348091
## [4,] 0.2265097 0.2553727          0.8869770
## [5,] 0.1808350 0.3007425          0.6012951
## [6,] 0.1554584 0.1972806          0.7880066

Guardando los resultados de la simulación

Antes

# Crear un data frame con los resultados
portf_results_base <- tibble(
  returns      = as.vector(portf_rtns_base),
  volatility   = as.vector(portf_vol_base),
  sharpe_ratio = as.vector(portf_sharpe_ratio_base)
)

portf_results_base <- data.frame(portf_results_base)

# Mostrar primeras filas
head(portf_results_base)

Después

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)

Frontera Eficiente

Antes

library(ggplot2)
library(dplyr)
library(viridis)


## Frontera eficiente (ESCENARIO BASE)

N_POINTS_base <- 200

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

portf_results_base <- data.frame(portf_results_base)

possible_ef_rtns_base <- seq(
  from = min(portf_results_base$returns_round),
  to   = max(portf_results_base$returns_round),
  length.out = N_POINTS_base
) %>% round(2)

# Frontera eficiente: mínima volatilidad por nivel de retorno
ef_df_base <- map_dfr(possible_ef_rtns_base, function(rtn) {
  subset <- filter(portf_results_base, returns_round == rtn)
  if (nrow(subset) > 0) {
    vol_min <- min(subset$volatility)
    tibble(returns = rtn, volatility = vol_min)
  } else {
    NULL
  }
})

# Activos individuales (escenario base)
individual_assets_df_base <- tibble(
  asset      = assets_base,
  return     = as.vector(avg_returns_base),
  volatility = sqrt(diag(cov_mat_base))
)

# Gráfico SOLO escenario base (igual estilo que el tuyo actual)
ggplot() +
  geom_point(
    data = portf_results_base,
    aes(x = volatility, y = returns, color = sharpe_ratio),
    alpha = 0.5, size = 1.3
  ) +
  geom_point(
    data = individual_assets_df_base,
    aes(x = volatility, y = return),
    shape = 23, size = 4, fill = "#2980b9", color = "black", stroke = 1
  ) +
  geom_text(
    data = individual_assets_df_base,
    aes(x = volatility, y = return, label = asset),
    hjust = -0.3, vjust = -0.1, size = 3.5
  ) +
  scale_color_viridis(name = "Sharpe Ratio", option = "C", direction = -1) +
  labs(
    title = "Frontera Eficiente (Escenario base - activos tecnológicos)",
    x = "Volatilidad Anual",
    y = "Retorno Esperado Anual"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title   = element_text(face = "bold", size = 15, color = "#2c3e50"),
    legend.position = "right"
  )

Despues

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 conjunto de gráficos muestra la frontera eficiente obtenida a partir de simulaciones masivas de portafolios construidos con seis acciones de distintos sectores (LLY, JNJ, AMD, ADBE, KHC y CAT), donde cada punto representa una combinación aleatoria de pesos. En ambos casos, la volatilidad anual se ubica en el eje X y el retorno esperado anual en el eje Y, mientras que el color de cada punto indica el índice de Sharpe: los tonos oscuros y cercanos al violeta corresponden a portafolios con mejor relación retorno-riesgo, y los tonos amarillos reflejan menor eficiencia. Con un millón de portafolios simulados ya se observa la clásica forma convexa de la frontera eficiente, pero la simulación de diez millones de portafolios permite una representación mucho más densa, continua y precisa del espacio de combinaciones posibles. En esta segunda gráfica la frontera superior se define con mayor claridad, pues aparecen más portafolios extremos y de alto Sharpe que antes no eran capturados. También se aprecia mejor el rol relativo de cada activo: ADB se sitúa cerca de la región de mayor eficiencia,donde los portafolios alcanzan mejores proporciones retorno-riesgo, lo que indica que incluir este activo en la combinación puede desplazar el portafolio hacia mayores retornos sin incrementar desproporcionadamente el riesgo. Esta ubicación estratégica sugiere que Adobe contribuye positivamente a mejorar el desempeño general del conjunto, especialmente en portafolios con orientación hacia rendimientos altos y riesgo controlado; mientras que AMD, CAT, KHC y JNJ se ubican en zonas de mayor volatilidad o menor retorno, generando portafolios menos eficientes cuando dominan la mezcla. En conjunto, la comparación muestra cómo un mayor número de simulaciones mejora la aproximación a la frontera eficiente verdadera, permite identificar con mayor precisión los portafolios óptimos y revela con más detalle la contribución individual de cada activo dentro del universo analizado.

En general, la diferencia gráfica entre la frontera eficiente generada con 1 millón de portafolios y la obtenida con 10 millones radica principalmente en el nivel de precisión y densidad con el que se representa el espacio de combinaciones posibles. En la simulación de 10 millones, la nube de puntos aparece mucho más completa, continua y suavemente distribuida, especialmente en la parte superior de la curva, donde se ubican los portafolios más eficientes; esto permite delinear con mayor claridad la verdadera frontera eficiente y capturar portafolios extremos que no alcanzan a aparecer con solo un millón de simulaciones. De igual manera, la gradación del Sharpe Ratio es más limpia y definida, mostrando concentraciones más evidentes de portafolios de alto desempeño. En contraste, la gráfica de 1 millón presenta una estructura correcta pero más dispersa, con huecos visibles y una frontera menos precisa, lo que refleja una menor exploración del espacio de posibles combinaciones.

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

Antes

# Portafolio con mayor Sharpe (base)
max_sharpe_port_base <- portf_results_base %>%
  filter(sharpe_ratio == max(sharpe_ratio)) %>%
  slice(1)

max_sharpe_port_base
# Portafolio con mínima varianza (base)
min_var_port_base <- portf_results_base %>%
  filter(volatility == min(volatility)) %>%
  slice(1)

min_var_port_base

###Despues

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

max_sharpe_port
# Portafolio con mínima varianza
min_var_port <- portf_results_df %>%
  filter(volatility == min(volatility)) %>%
  slice(1)
min_var_port

Graficando los portafolios anteriores

Antes

ggplot() +
  geom_point(data = portf_results_base,
             aes(x = volatility, y = returns, color = sharpe_ratio),
             alpha = 0.5, size = 1.2) +
  geom_point(data = individual_assets_df_base,
             aes(x = volatility, y = return),
             shape = 23, size = 4, fill = "#2980b9", color = "black") +
  geom_text(data = individual_assets_df_base,
            aes(x = volatility, y = return, label = asset),
            hjust = -0.3, vjust = -0.2, size = 3.5) +
  geom_point(data = max_sharpe_port_base,
             aes(x = volatility, y = returns),
             color = "gold", shape = 17, size = 4) +
  geom_text(data = max_sharpe_port_base,
            aes(x = volatility, y = returns, label = "Máx. Sharpe (base)"),
            hjust = -0.2, vjust = 1.5, color = "gold", size = 3.5) +
  geom_point(data = min_var_port_base,
             aes(x = volatility, y = returns),
             color = "midnightblue", shape = 15, size = 4) +
  geom_text(data = min_var_port_base,
            aes(x = volatility, y = returns, label = "Mín. Varianza (base)"),
            hjust = -0.2, vjust = 1.5, color = "midnightblue", size = 3.5) +
  scale_color_viridis(name = "Sharpe Ratio", option = "C", direction = -1) +
  labs(
    title = "Portafolios óptimos - Escenario base",
    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"))

Despues

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.

Al comparar las fronteras eficientes generadas con un millón y con diez millones de portafolios simulados, se observa que aumentar el número de simulaciones no solo hace la frontera más densa y suave, sino que también corrige la estimación de los portafolios óptimos. En el escenario de un millón de portafolios, el portafolio de mínima varianza aparecía con un retorno superior al 0,1%, mientras que en la simulación de diez millones, ese retorno se ajustó a alrededor del 0,1%, indicando que la estimación inicial sobrestimaba ligeramente el rendimiento mínimo posible. Una situación similar ocurre con el portafolio de máximo Sharpe: en la frontera de un millón, su retorno resultaba superior al 0,3%, pero al aumentar la simulación a diez millones, el retorno se estabiliza también alrededor de un valor ligeramente por encima del 0,3%, reflejando un ajuste más preciso y menos disperso. En conjunto, esto muestra que, aunque la forma general de la frontera se mantiene, la simulación más extensa proporciona valores más refinados, reduce sesgos de muestreo y ubica los portafolios óptimos en posiciones más consistentes con la verdadera frontera eficiente.

Encontrando las ponderaciones de los portafolios óptimos

Semilla (42) - 1 Millon de Iteraciones

### Semilla (42) - 1 Millón
# Índices de los portafolios óptimos
idx_max_sharpe_base <- which.max(portf_results_base$sharpe_ratio)
idx_min_var_base    <- which.min(portf_results_base$volatility)

# Pesos óptimos
pesos_max_sharpe_base <- weights_base[idx_max_sharpe_base, ]
pesos_min_var_base    <- weights_base[idx_min_var_base, ]

tabla_42_1M <- data.frame(
  Activo = assets_base,
  `Máx. Sharpe` = round(pesos_max_sharpe_base, 4),
  `Mín. Varianza` = round(pesos_min_var_base, 4)
)

knitr::kable(tabla_42_1M,
             caption = "Pesos por Activo – Semilla 42 / 1M Iteraciones")
Pesos por Activo – Semilla 42 / 1M Iteraciones
Activo Máx..Sharpe Mín..Varianza
ADBE 0.0023 0.0577
AMD 0.0892 0.0101
CAT 0.3856 0.0995
JNJ 0.0112 0.5919
KHC 0.0085 0.1514
LLY 0.5031 0.0894

Semilla (42) - 5 millones de Iteraciones

### Semilla (42) - 5 Millones

N_PORTFOLIOS_42_5M <- 5 * 10^6  
set.seed(42)

weights_raw_42_5M <- matrix(
  runif(N_PORTFOLIOS_42_5M * n_assets_base),
  nrow = N_PORTFOLIOS_42_5M,
  ncol = n_assets_base
)

weights_42_5M <- weights_raw_42_5M / rowSums(weights_raw_42_5M)

# Métricas
portf_rtns_42_5M <- weights_42_5M %*% avg_returns_base
portf_vol_42_5M  <- apply(weights_42_5M, 1, function(w) sqrt(t(w) %*% cov_mat_base %*% w))
portf_sharpe_42_5M <- portf_rtns_42_5M / portf_vol_42_5M

# Índices óptimos
idx_max_sharpe_42_5M <- which.max(portf_sharpe_42_5M)
idx_min_var_42_5M    <- which.min(portf_vol_42_5M)

# Pesos óptimos
pesos_max_sharpe_42_5M <- weights_42_5M[idx_max_sharpe_42_5M, ]
pesos_min_var_42_5M    <- weights_42_5M[idx_min_var_42_5M, ]

tabla_42_5M <- data.frame(
  Activo = assets_base,
  `Máx. Sharpe` = round(pesos_max_sharpe_42_5M, 4),
  `Mín. Varianza` = round(pesos_min_var_42_5M, 4)
)

knitr::kable(tabla_42_5M,
             caption = "Pesos por Activo – Semilla 42 / 5M Iteraciones")
Pesos por Activo – Semilla 42 / 5M Iteraciones
Activo Máx..Sharpe Mín..Varianza
ADBE 0.0019 0.0957
AMD 0.1085 0.0164
CAT 0.2940 0.1116
JNJ 0.0159 0.5824
KHC 0.0020 0.1310
LLY 0.5776 0.0629

Semilla (30) - 500 mil iteraciones

### Semilla (30) - 500 Mil

N_PORTFOLIOS_30_500K <- 5 * 10^5  
set.seed(30)

weights_raw_30_500K <- matrix(
  runif(N_PORTFOLIOS_30_500K * n_assets),
  nrow = N_PORTFOLIOS_30_500K,
  ncol = n_assets
)

weights_30_500K <- weights_raw_30_500K / rowSums(weights_raw_30_500K)

# Métricas
portf_rtns_30_500K <- weights_30_500K %*% avg_returns
portf_vol_30_500K  <- apply(weights_30_500K, 1, function(w) sqrt(t(w) %*% cov_mat %*% w))
portf_sharpe_30_500K <- portf_rtns_30_500K / portf_vol_30_500K

# Índices óptimos
idx_max_sharpe_30_500K <- which.max(portf_sharpe_30_500K)
idx_min_var_30_500K    <- which.min(portf_vol_30_500K)

# Pesos óptimos
pesos_max_sharpe_30_500K <- weights_30_500K[idx_max_sharpe_30_500K, ]
pesos_min_var_30_500K    <- weights_30_500K[idx_min_var_30_500K, ]

tabla_30_500K <- data.frame(
  Activo = assets,
  `Máx. Sharpe` = round(pesos_max_sharpe_30_500K, 4),
  `Mín. Varianza` = round(pesos_min_var_30_500K, 4)
)

knitr::kable(tabla_30_500K,
             caption = "Pesos por Activo – Semilla 30 / 500K Iteraciones")
Pesos por Activo – Semilla 30 / 500K Iteraciones
Activo Máx..Sharpe Mín..Varianza
ADBE 0.5558 0.0536
AMD 0.0246 0.5911
CAT 0.1047 0.0372
JNJ 0.0108 0.0705
KHC 0.0052 0.1277
LLY 0.2988 0.1200

Semilla (30) - 10 millones de iteraciones

### Semilla (30) - 10 Millones

idx_max_sharpe <- which.max(portf_results_df$sharpe_ratio)
idx_min_var    <- which.min(portf_results_df$volatility)

pesos_max_sharpe <- weights[idx_max_sharpe, ]
pesos_min_var    <- weights[idx_min_var, ]

tabla_30_10M <- data.frame(
  Activo = assets,
  `Máx. Sharpe` = round(pesos_max_sharpe, 4),
  `Mín. Varianza` = round(pesos_min_var, 4)
)

knitr::kable(tabla_30_10M,
             caption = "Pesos por Activo – Semilla 30 / 10M Iteraciones")
Pesos por Activo – Semilla 30 / 10M Iteraciones
Activo Máx..Sharpe Mín..Varianza
ADBE 0.5326 0.0513
AMD 0.0053 0.5950
CAT 0.0985 0.0217
JNJ 0.0008 0.0824
KHC 0.0050 0.1385
LLY 0.3578 0.1111

La tabla muestra la asignación de pesos para cada uno de los seis activos 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.

Se compararon inicialmente los pesos tomando en cuenta la semilla 42, con 1 millón de simulaciones, y luego 5 millones, y se encontró que, el porcentaje de peso de los activos en los portafolios no presentan gran diferencia. Por ejemplo, si nos enfocamos en el activo LLY, tanto en 1 millon de simulaciones como en 5 mill, el porcentaje de peso que maximiza el sharpe ratio es 0.5031 y 0.5776 respectivamente (no muestra diferencias significativas).

El mismo resultado arroja cuando se analiza la semilla 30 con 500k interacciones y 10 mill de interacciones. Por ejemplo, en el caso del activo ADBE, el porcentaje de peso optimo es de 0.5558 en 500k interacciones, mientras que en 10 mill es de 0.5326.

Esto nos deja en evidencia que, incrementar el numero de simulaciones no va a cambiar el porcentaje de peso de los activos en gran magnitud (si se usa la misma semilla), si no que, en cambio, usar otra semilla es la que dará porcentajes de peso completamente distintos.

Exactamente la misma situación se presenta al evaluar los pesos tomando en cuenta la minima varianza.

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 permitió evaluar el comportamiento de seis activos pertenecientes a distintos sectores y construir portafolios eficientes bajo la metodología de la teoría moderna de portafolios. La transformación de precios en rendimientos logarítmicos demostró ser un paso fundamental para garantizar la estacionariedad de las series y obtener medidas de riesgo y retorno consistentes. Esto permitió trabajar con insumos estadísticamente sólidos al momento de estimar retornos promedio y matrices de covarianza anualizadas, que constituyen la base para identificar combinaciones óptimas dentro del universo de portafolios simulados.

El estudio exploró la estructura temporal de los retornos en diferentes frecuencias —por día de la semana, mes y año—, mostrando que no existe evidencia clara de estacionalidades marcadas. Sin embargo, se observan diferencias en la volatilidad según el tipo de activo: empresas tecnológicas como AMD y ADBE exhiben una variabilidad significativamente mayor, mientras que sectores defensivos como salud y consumo básico presentan retornos más estables. Esta distinción refuerza la importancia de la diversificación sectorial para mitigar riesgos dentro del portafolio.

La comparación entre simulaciones de distinta magnitud evidenció que aumentar el número de portafolios generados mediante Monte Carlo mejora la precisión con la que se aproxima la frontera eficiente. Aunque la forma general de la frontera ya es visible con un millón de portafolios, la simulación de diez millones proporciona una representación más densa y continua, disminuye el error por muestreo y captura portafolios extremos que inicialmente no aparecían. Esto se reflejó también en una estimación más estable tanto del portafolio de mínima varianza como del portafolio de máximo Sharpe, cuyos retornos se ajustaron a valores más realistas cuando se incrementó la escala de la simulación.

Un hallazgo relevante es que, manteniendo la misma semilla, incrementar el número de simulaciones no produce cambios sustanciales en los pesos óptimos asignados a cada activo. Las diferencias son pequeñas y se mantienen dentro de rangos coherentes. En contraste, modificar la semilla sí genera variaciones más visibles en la estructura de pesos, lo cual confirma que el punto exacto de la simulación depende de la secuencia inicial de números aleatorios utilizada, aunque la lógica del método y las conclusiones generales permanecen estables. Esto refuerza la idea de que la frontera eficiente estimada es robusta, y que las diferencias en los portafolios óptimos responden principalmente a la aleatoriedad de la simulación y no a fallas metodológicas.

Finalmente, el análisis de los portafolios óptimos muestra la clásica disyuntiva entre eficiencia y seguridad. El portafolio de máximo Sharpe ofrece la mejor combinación retorno-riesgo y beneficia especialmente a inversionistas con mayor tolerancia al riesgo, mientras que el portafolio de mínima varianza se posiciona como la alternativa más conservadora, priorizando estabilidad sobre rentabilidad. La comparación entre ambos portafolios, junto con la evaluación gráfica de sus ubicaciones en la frontera eficiente, evidencia cómo diferentes combinaciones de activos pueden adaptarse a distintos perfiles de inversión.

En conjunto, el trabajo demuestra que la teoría de portafolios, aplicada mediante simulaciones extensas y utilizando rendimientos adecuadamente transformados, permite obtener conclusiones robustas sobre el comportamiento del riesgo-retorno, la contribución individual de los activos y la construcción de carteras eficientes. La metodología empleada es consistente, replicable y estadísticamente sólida, y evidencia la importancia de la diversificación y del análisis cuantitativo para la toma de decisiones financieras informadas.