Optimización de portafolios en R
- Ángelo Lizama
- 23 sept 2024
- 7 Min. de lectura
A continuación, entregamos un análisis de optimización de portafolio con programación R. Aquí se busca, en términos simples, minimizar la volatilidad de un portafolio dado un nivel de rentabilidad o en su defecto, dado un nivel de rentabilidad maximizamos el ratio de sharpe que corresponde a la rentabilidad por unidad de riesgo.
Mediante la simulación de portafolios y la obtención de la frontera eficiente lograrás obtener los gráficos que adjunto a continuación, y a su vez, puntos de interés como es el portafolio con máximo ratio de sharpe así como el portafolio de mínima varianza. Adicionalmente, podrás graficar los portafolios en función del ratio de sharpe y podrás visualizar el cómo afecta la incorporación y combinación de acciones a la eficiencia de tu portafolio.
Este modelo no solamente da luces en el ámbito de las inversiones si no que también de la gestión, a modo de ejemplo, si una empresa desea llevar a cabo proyectos que son independientes pero con baja probabilidad de éxito, le conviene llevar a cabo varios proyectos puesto que reduce la probabilidad de pérdida (diversificación), para ver más detalle te adjuntamos una nota al respecto en el siguiente link.
De ante mano pedimos las disculpas por la no utilización de acentos en el código, lo cual puede llevar a confusión, en algunos casos, en su explicación.
Adjuntamos algunas consideraciones:
Obtenemos precios diarios desde Yahoo, en particular los precios ajustados, por lo que debes estar conectado a internet. El análisis es diario y se anualiza por 250 días que corresponderían a los días que se transa en un año. No se considera tasa de libre riesgo o en su defecto es 0%. Debes instalar las librerías que se encuentran descritas en el código mediante la función install.packages() y activarlas con library(). Finalmente, utilizamos la versión de R 4.3.1.
Adjuntamos el código con una breve explicación de la programación:
library(rvest)library(dplyr)library(quantmod)library(purrr)library(tidyverse)library(timetk)library(lubridate)library(PerformanceAnalytics)library(highcharter)library(nloptr)library(tidyr)#Extraccion de datos desde Yahooread_html_tables <- function(url){ webpage <- read_html(url) tables <- html_table(webpage, fill=T) return(tables)}#se define la url en donde se encuentra la dataurl <-"https://es.wikipedia.org/wiki/%C3%8Dndice_de_Precio_Selectivo_de_Acciones"#Se asignan tablas a la variable "tables"tables <- read_html_tables(url)#######Importate######seleccinamos las acciones que deseamos analizar#Se pueden añadir más acciones#Ojo que las acciones deben estar en la url definidas puesto que de aquí se obtiene la data# A modo de ejemplo, queremos analizar más acciones, implica que debemos cambiar esto# c("COPEC","CHILE","CENCOSUD","FALABELLA", "ENTEL", "IAM", "PARAUCO") etc# si cambias el tamaño de este vector, el programa lo detecta y no debes añadir nada mas al codigoipsa <- c("COPEC","CHILE","CENCOSUD", "IAM","PARAUCO")# Fecha de inicio de extraccion de datastart_date <- "2020-01-01"# Fecha de terrmino de extraccion de dataend_date <- "2024-08-31"#Bajando data de Yahoo a traves de una funciondownload_data <- function(symbol){ stock_data <- getSymbols(Symbols = paste0(symbol, ".SN"), src = "yahoo", from =start_date, to = end_date, auto.assign= FALSE) %>% data.frame()%>% rownames_to_column(var="Date")%>% mutate(Symbol = symbol) return(stock_data)}# la funcion map recoge un simbolo en el IPSA y lo aplica a la funcion download_data()# actual como una "for" es decir, para cada elemento de IPSA aplica la funcion.# es mas simple que realizar un for# aqui se crea una lista con cada acciondata_list <- map(ipsa, download_data)#tenemos un problema Houston puesto que la columa Date es un caracter y debemos pasarla a fecha #con la funcion ymdfor (h in 1:length(ipsa)) { data_list[[h]]$Date <- ymd(data_list[[h]]$Date)}#en este codigo convertimos la data en serie de tiempo, puesto que asi es mas simple gráficar en highchartsfor (h in 1:length(ipsa)) { data_list[[h]] <- tk_xts(data_list[[h]],data_var = data_list[[h]]$Date)}# eliminamos columnas en cada data frame de cada accion# en este caso nos quedamos con el precio ajustado# si quieres hacer un análisis con el precio de cierre debes utilizar esto# data_list <- sapply(data_list, FUN = function(x) subset(x, select=-c(1,2,3,5,6)), simplify = FALSE)# dado que estamos realizando un analisis de largo plazo, se debe trabajar con el precio ajustado# si quieres analizar un escenario de corto plazo, debes trabajar con el precio de cierre# de esta forma, rescatas el comportamiento del precio sin ajuste de dividendosdata_list <- sapply(data_list, FUN = function(x) subset(x, select=-c(1,2,3,4,5)), simplify = FALSE)# fusionamos la lista en un dataframe llamado all_dataall_data <- do.call(cbind, data_list)#Reasignamos nombresnames(all_data)<- ipsa# obtenemos retornos simples (Xt/xt-1) omitiendo los NA que se produce al perder un datoretornos <- Return.calculate(all_data, method = "simple")%>%na.omit()################### Simulacion de portafolios################### simularemos 1500 portafoliosnumportfolio <- 1500#generamos una lista de dimension de 1500 para recoger cada portafoliomatriz_port <- list(1:numportfolio)#Creamos una funcion para realizar la simulacionportfolio_simulacion <- function (x) { # creamos listas para recibir la data que se generara rets <- list(1) vols <- list(1) wts <- list(1) # lo que esta dentro del for se repitira 1500 veces for (i in 1: numportfolio) { #Se genera numeros aleatorios en funcion de la cantidad de acciones weights_port <- runif(ncol(x)) #Se genera ponderadores que sumen 1 weights_port <- weights_port/sum(weights_port) # el retorno esperado del portafolio es igual a las ponderaciones x retorno de cada accion # Sacamos retorno anualizado y multiplicamos por 250, ya que el retorno es diario # 250 serían los días de transacción promedio en un año, de esta forma anualizamos retornos y volatilidad # para llegar a 250 se debe considerar dias de transaccion de lunes a viernes, restando feriados # el retorno anualizado es simplemente 250 x retorno diario # la volatilidad anualizada es la desviacion estandar del retorno diario multiplicada por raiz de 250 rets<- t(round(colMeans(x)*250, digits = 2))%*%weights_port #La volatilidad se obtiene de la matriz de covarianza por los ponderadores # se multiplica por raiz de 250, de esta forma obtenemos la volatilidad anual de cada portafolio vols<- sqrt(t(weights_port)%*%cov(x)%*%weights_port)*sqrt(250) # Se asignan los ponderadores a la variable wts wts <- weights_port # se traspone y se conviere en un data frame wts <- as.data.frame(t(wts)) names(wts) <- ipsa # se genera un data frame con el retorno y la volatilidad data_port <- data.frame(rets, vols) # se une en un data frame data_port y wts data_port <- cbind(data_port,wts) # cada repeticion en donde se obtiene data_port se asigna a matriz_port 1500 veces matriz_port[[i]]<- data_port } # la funcion portfolio_simulacion entrega una lista de 1500 datos con ponderadores # retornos y volatilidad return(matriz_port)}# aplicamos la funcion a "retornos" y obtenemos matriz_port y se asigna a temptemp <- portfolio_simulacion(retornos)# unimos cada lista en un data framedf_unido <- do.call(rbind, temp)# calculamos ratio de sharpe y lo añaidmos a df_unido,considerando tasa de libre riesgo 0%df_unido$sharp_r <- df_unido$rets/df_unido$vols# dato de interes, cual es la cartera con mayor ratio de sharpeubicacion <- which(df_unido$sharp_r == max(df_unido$sharp_r))# Extraemos los podneradores de la cartera optimaCartera_optima <- df_unido[ubicacion,]#Graficamos el resultado# multiplicamos x 100 para dejar los valores en %# se grafica en funcion del ratio de sharpe# mientras mas grande el ratio se obtiene un color mas violetaggplot(df_unido, aes(x = vols*100, y = rets*100, color = sharp_r)) + geom_point() + scale_color_gradientn(colors = c("cyan", "violet"), limits = c(min(df_unido$sharp_r, na.rm = TRUE), max(df_unido$sharp_r, na.rm = TRUE)), name = "Sharp Ratio") + theme_minimal()+ labs(x = "Volatilidad Anualizada (%)", y = "Retorno Anualizado(%)") ###############Proceso de optimización###############################Frontera eficiente################# generamos una funcion para calcular retornos y volatilidad de un portafolio# aqui obtenemos el ratio de sharpe y se coloca el signo negativo para minimizar# al colocar el signo negativo estariamos minimizando la funcion pero en realidad es el maximoportfolio_opt <- function(weights_port){ #weights_port <- matrix(weights_port) p_rets<- t(round(colMeans(retornos)*250, digits = 2))%*%weights_port p_vols<- sqrt(t(weights_port)%*%cov(retornos)%*%weights_port)*sqrt(250) return(-p_rets/p_vols) }# Ponderadores inicialesint_w <- rep(1, ncol(retornos))/ncol(retornos)# restricciones para cada accion, que va de 0 a 100% en ponderacion# limite inferior de 0, es decir, no permitimos irnos corto en acciones o ponderadores negativoslower_bounds <- rep(0,ncol(retornos))# limite superior de 1 o 100%, es decir, una accion puede tener el 100%upper_bounds <- rep(1,ncol(retornos))# restriccionesconstraint_function <- function(weights_port){ #suma de ponderadores igual a 100% ponderador <-(sum(weights_port)-1) #ponderadores x retornos debe ser igual al retorno objetivo r_target <- (sum(t(weights_port) %*% colMeans(retornos)*250) - target_return) return(c(ponderador, r_target))}# generamos una lista de 30 para recoger la data que se generarafrontera_ef <- list(1:30)# seteamos las rentabilidad entre 2% y 25%, generando 30 datos entre ellos, incluyendolos# importante esta secuencia puesto que si cambias de activos, debes ver si dichas rentabilidades # se ajustan a los resultadostarget <-seq(0.02,0.25, length= 30)# como tenemos una lista de 30, realizamo un "for" para 30# recogemos el retorno objetivo# minimizamos el ratio de sharpe que dado que es negativo se maximiza# del resultado sacamos el portafolio optimo y lo guardamos en mtx# vola rescata la volatilidad del portafolio optimo anualziado# generamos la frontera eficiente rescatando el retorno, la volatilidad y el ratio de sharpefor (i in 1:30) { target_return <- target[i] #modelo para minimizar mediante el metodo SLSQP opt_sharpe_sls <- slsqp(x0 = int_w,fn=portfolio_opt, lower = lower_bounds, upper=upper_bounds, heq =constraint_function, control = list(xtol_rel = 1e-4, check_derivatives = TRUE)) mtx <- as.matrix(opt_sharpe_sls[["par"]]) vola <- sqrt(t(mtx)%*%cov(retornos)%*%mtx)*sqrt(250) frontera_ef[[i]] <- c(target[i],vola, target[i]/vola, t(mtx)) }#unimos la lista en un solo data framefrontera_unido <- as.data.frame(do.call(rbind, frontera_ef) )names(frontera_unido) <- c("retorno","volatilidad","sharpe",t(ipsa))#grafico de portafolios simuladosggplot(frontera_unido, aes(x = volatilidad*100, y = retorno*100, color = sharpe)) + geom_point() + scale_color_gradientn(colors = c("violet", "yellow"), limits = c(min(frontera_unido$sharpe, na.rm = TRUE), max(frontera_unido$sharpe, na.rm = TRUE)), name = "Sharp Ratio") + theme_minimal()+ labs(x = "Volatilidad Anualizada (%)", y = "Retorno Anualizado(%)") #Cartera max ratio de sharpelocation <- which(frontera_unido$sharpe == max(frontera_unido$sharpe))# Extraemos los podneradores de la cartera optimaCartera_max_sharpe <- frontera_unido[location,]#Cartera minima varianzalocation_v <- which(frontera_unido$volatilidad == min(frontera_unido$volatilidad))Cartera_min_var <- frontera_unido[location_v,]for (i in 1:30) { target_return <- target[i] opt_sharpe <- nloptr(x0 = int_w, eval_f = portfolio_opt, lb =lower_bounds, ub = upper_bounds, opts = options, eval_g_eq = constraint_function) mtx <- as.matrix(opt_sharpe[["solution"]]) vola <- sqrt(t(mtx)%*%cov(retornos)%*%mtx)*sqrt(250) frontera_ef[[i]] <- c(target[i],vola, target[i]/vola) }frontera_grap <- frontera_unido[,c(-1,-2)]retornos_largo <- frontera_grap %>% pivot_longer(cols = -sharpe, names_to = "ACCIONES", values_to = "Ponderacion")#graficamos cada portafolio con su ratio de sharpeggplot(retornos_largo, aes(x = sharpe, y = Ponderacion, fill = ACCIONES)) + geom_bar(stat = "identity", width= 0.01) + labs(title = "Ponderación de Acciones", x = "Sharp Ratio", y = "Ponderación") + theme_minimal()#gráficamos los precios de cada accion normalizado en 0names(data_list)<- ipsahchart_obj<- highchart(type = "stock") %>% hc_title(text = "Gráfico comparativo") %>% hc_xAxis((type="datatime"))%>% hc_tooltip(shared =T, crosshairs=T)%>% hc_add_theme(hc_theme_db())for (i in seq_along(data_list)) { hchart_obj <- hchart_obj %>% hc_add_series(data_list[[i]], name = names(data_list)[i], compare="percent")}hchart_objEsperamos que haya sido útil.
Cualquier duda, no dudes en contactarnos.
Saludos
Equipo Principia…












Comentarios