Como seria uma carteira com as “melhores” ações?

Um simples exemplo de otimização de carteira de ações.

A carteira de Markowitz

A teoria moderna da carteira (MPT) afirma que os investidores são avessos ao risco e, dado um nível de risco, escolherão as carteiras que mais oferecem retorno. Para isso, precisamos otimizar os portfólios.

Para realizar a otimização, precisaremos:

  • Baixar os dados de preços dos ativos
  • Calcular os retornos médios para o período de tempo desejado
  • Atribuir pesos aos ativos e, em seguida, usá-los para construir uma fronteira eficiente

Ações que mais subiram em setembro de 2020

Carregamento dos pacotes

library(tidyquant) # Para obter os dados
library(timetk)    # Para manipular as séries temporais
library(tidyverse) # Nosso velho amigo

Vamos obter os dados de preços das ações.

acoes <- c('AZUL4.SA', 'GGBR4.SA', 'PCAR3.SA', 'QUAL3.SA', 'RENT3.SA')

inicio <- '2020-09-01'

fim <- '2020-09-30'

# tq(get() carrega dados no formato 'tibble'
precos <- tq_get(acoes,
                 from = inicio,
                 to = fim,
                 get = 'stock.prices')

Cálculo dos retornos (logarítimicos - contínuos).

Nota: Tente tq_transmute_fun_options() para conhecer as opções. São muitas!

Para a função periodReturn veja este link.

ret_log_tidy <- precos %>%
    group_by(symbol) %>%
    tq_transmute(select = adjusted,
                 mutate_fun = periodReturn,  
                 period = 'daily', # retornos diários
                 col_rename = 'ret',
                 type = 'log')

ret_log_tidy

Os dados estão em formato tibble, ou tidy. Usaremos a função spread() para convertê-los em um formato wide. Também vamos convertê-los em um objeto de série temporal usando a função tk_xts() e calcular as médias dos retornos diários.

ret_log_xts <- ret_log_tidy %>%
    spread(symbol, value = ret) %>%
    tk_xts()

head(ret_log_xts, 5)
##               AZUL4.SA    GGBR4.SA     PCAR3.SA    QUAL3.SA     RENT3.SA
## 2020-09-01 0.000000000  0.00000000  0.000000000  0.00000000  0.000000000
## 2020-09-02 0.009989182 -0.01055546 -0.005877774  0.02770961  0.012814300
## 2020-09-03 0.036074398 -0.02093526 -0.026567609 -0.02102698 -0.007076885
## 2020-09-04 0.024702779  0.02748270  0.007934031  0.07394938  0.001182944
## 2020-09-08 0.065707013 -0.01161331 -0.012404707  0.01374885  0.057059892
autoplot(ret_log_xts) +
  ggtitle('Retornos diários (log)') +
  xlab('dia')

ret_medio <- colMeans(ret_log_xts)
print(round(ret_medio, 5))
## AZUL4.SA GGBR4.SA PCAR3.SA QUAL3.SA RENT3.SA 
##  0.00222  0.00052  0.00427  0.00159  0.00605

Em seguida, vamos calcular a matriz de covariância para todas essas ações. Vamos anualizar os valores multiplicando por 252 (dias úteis no anos).

Passo-a-passo

Antes de aplicarmos nossos métodos a milhares de portfólio aleatórios, vamos demonstrar os passos em um único portfólio.

Para calcular os retornos e riscos da carteira (medidos pelo desvio-padrão) precisaremos calcular:

  • Retornos médios de ativos
  • Pesos do portfólio
  • A matriz de covariância de todos os ativos
  • Os pesos aleatórios

Vamos calcular a matriz de covariância e criar os pesos aleatórios.

mat_cov <- cov(ret_log_xts) * 252
AZUL4.SA GGBR4.SA PCAR3.SA QUAL3.SA RENT3.SA
AZUL4.SA 0.3563967 0.0348714 0.0110947 0.0538900 0.1635246
GGBR4.SA 0.0348714 0.1234851 -0.0420657 0.0447651 0.0403522
PCAR3.SA 0.0110947 -0.0420657 0.3586795 -0.0210142 -0.1821510
QUAL3.SA 0.0538900 0.0447651 -0.0210142 0.1924192 0.0323458
RENT3.SA 0.1635246 0.0403522 -0.1821510 0.0323458 0.3965523
pesos <- runif(n = length(acoes))
print(pesos)
## [1] 0.6549605 0.5102920 0.1500982 0.8704479 0.5144417
print(sum(pesos))
## [1] 2.70024

Vamos tornar a soma dos pesos igual a 1

pesos <- pesos/sum(pesos)
print(pesos)
## [1] 0.24255638 0.18898022 0.05558698 0.32235942 0.19051700
print(sum(pesos))
## [1] 1

O retorno anualizado da carteira (252 dias úteis)

\[ \begin{aligned} & \bar r_{diário} = \sum_i^ n \, (peso_i \,\times \, \bar r_i) \\ & r_{carteira} = (\bar r_{diário} \, + \, 1)^{252} \, - \, 1 \end{aligned} \]

ret_carteira <- (sum(pesos * ret_medio) + 1)^252 - 1
ret_carteira * 100
## [1] 89.46824

O retorno anualizado também pode ser aproximado considerando o retorno diário (logarítimico) como um retorno contínuo, utilizando a fórmula:

\[r_{carteira} = e^ { \; (\bar r_{diário} \, \times \, {252})} \; - \, 1\]

ret_carteira_continuo <- exp(sum(pesos * ret_medio) * 252) - 1
ret_carteira_continuo * 100
## [1] 89.62195

Em seguida, vamos calcular o risco da carteira (desvio-padrão).

Um pouco de álgebra linear não faz mal a ninguém.

\[\sigma_{carteira} = (p^t \, \Sigma \, p)^{1/2}\]

risco_carteira <- sqrt(t(pesos) %*% (mat_cov %*% pesos))

# Apresentação mais elegante no console utilizando o pacote glue.
glue::glue("Risco da carteira: {round(risco_carteira*100, 2)}", "%")
## Risco da carteira: 30.78%

Utilizando o índice de Sharpe

rf <- 0 # Renda Fixa

indice_sharpe <- (ret_carteira - rf) / risco_carteira

glue::glue("Índice Sharpe da carteira: {round(indice_sharpe*100, 2)}", "%")
## Índice Sharpe da carteira: 290.66%

Temos tudo o que precisamos para realizar a otimização. O que precisamos fazer agora é executar este código em 5000 carteiras aleatórias. Para isso usaremos um loop.

Antes de fazermos isso, precisamos criar vetores vazios e uma matriz para armazenar nossos valores.

num_cart <- 5000

# Cria uma matriz para armazenar os pesos

todos_pesos <- matrix(nrow = num_cart,
                      ncol = length(acoes))

# Cria um vetor vazio para armazenar os retornos da carteira

ret_carteira <- vector('numeric', length = num_cart)

# Cria um vetor vazio para armazenar os desvios-padrão da carteira 

risco_carteira <- vector('numeric', length = num_cart)

# Cria um vetor vazio para armazenar os índices de Sharpe da carteira 

indice_sharpe <- vector('numeric', length = num_cart)

Um loop 5000 vezes:

id <- diag(length(acoes))

for (i in seq_along(ret_carteira)) {
    
    if (i <= length(acoes)) # carteiras com apenas uma ação
      pesos <- id[i,]
    else  
      pesos <- runif(length(acoes))
    
    pesos <- pesos/sum(pesos)
   
    # Guarda os pesos em uma matriz
    todos_pesos[i,] <- pesos
    
    # Retornos das carteiras
    
    port_ret <- sum(pesos * ret_medio)
    port_ret <- ((port_ret + 1)^252) - 1
    
    # Salvando os retornos das carteiras
    ret_carteira[i] <- port_ret
    
    # Cria e salva os riscos das carteiras
    port_sd <- sqrt(t(pesos) %*% (mat_cov  %*% pesos))
    risco_carteira[i] <- port_sd
    
    # Cria e salva os índices de Sharpe das carteiras
    
    i_sharpe <- (port_ret - rf) / port_sd
    indice_sharpe[i] <- i_sharpe
    
}

Vamos armazenar os valores, converter a matriz em um ´tibble`, alterar os nomes das variáveis e combinar os valores

valores_carteira <- tibble(Retorno = ret_carteira,
                           Risco = risco_carteira,
                           IndiceSharpe = indice_sharpe)


todos_pesos <- tk_tbl(todos_pesos)

colnames(todos_pesos) <- colnames(ret_log_xts)

valores_carteira <- tk_tbl(cbind(todos_pesos, valores_carteira))

valores_carteira

Temos os pesos de cada ativo com os riscos e os retornos, juntamente com o índice Sharpe de cada carteira.

Em seguida, vamos olhar para as carteiras que mais importam:

  • A carteira de variância mínima
  • A carteira de tangência (a carteira com maior índice sharpe)
min_var <- valores_carteira[which.min(valores_carteira$Risco),]
max_i_sharpe <- valores_carteira[which.max(valores_carteira$IndiceSharpe),]

Vamos plotar os pesos de cada carteira. Primeiro com a carteira de variância mínima.

Como podemos observar, a carteira de variância mínima quase não tem alocação para as ações da Azul. A maior parte da carteira é investida em ações da Gerdau.

Em seguida, vamos olhar para a carteira de tangência ou a carteira com o maior índice Sharpe.

Que diferença! Localiza e Pão de Açúcar agora dominam completamente a carteira e Gerdau tem uma pequena participação.

Vamos calcular novamente o retorno e o risco anual de cada ação individualmente:

nomes <- c('Azul', 'Gerdau', 'Localiza', 'PAO', 'Qualicorp')
ativos <- data.frame(Acoes = nomes, 
                     Retorno = (colMeans(ret_log_xts, na.rm = TRUE)+ 1)^252 - 1, 
                     Risco = sapply(ret_log_xts, sd, na.rm = TRUE) * sqrt(252))
ativos

A fronteira eficiente

Segundo a teoria moderna de porfolio, desenvolvida por Markowitz em 1952, podemos definir o problema de seleção de uma carteira como o seguinte problema de otimização quadrática:

\[ \begin{aligned} \underset {p} {min} &= p^T \; \Sigma \; p \\ s.a. \\ p^T \hat \mu &= \bar r \\ p^T \; 1 &= 1 \end{aligned} \]

A expressão indica que minimizamos o risco induzido pela variância-covariância \(\small \sigma^2 = p^T \; \Sigma \; p\), onde a matriz \(\small \Sigma\) é uma estimativa da covariância entre os ativos da carteira. O vetor p denota as participações (pesos) dos investimentos individuais sujeitos à condição \(\small p^T 1=1\), ou seja, que o capital disponível está totalmente investido. O retorno esperado ou retorno-alvo \(\small \bar r\) é expresso pela condição \(\small p^T \hat \mu = \bar r,\) onde o vetor n-dimensional \(\small \hat \mu\) estima a média esperada dos ativos. O portfólio com vendas curtas (short-selling) ilimitadas podem ser resolvidos analiticamente. No entanto, se os pesos não podem ser negativos, o que significa a impossibilidade de vendas curtas, então a otimização tem que ser feita numericamente. A estrutura do problema é quadrática e, portanto, podemos usar um otimizador quadrático para calcular os pesos da carteira.

Consideremos o seguinte problema padrão:

\[ \begin{aligned} \underset {p} {min} &= p^T \; \Sigma \; p \\ s.a. \\ A \; p & \leq b \\ p^T 1 &= 1 \end{aligned} \]

Pode-se mostrar que, se \(\small \Sigma\) é uma matriz definida positiva, o problema do portfólio de Markowitzé um problema de otimização convexa. Como tal, as soluções ótimas locais também são soluções ótimas globais. O pacote quadprog fornece a função solve.QP(), que provê uma interface uma subrotina FORTRAN. Esta subrotina implementa o método dual de Goldfarb e Idnani para resolver problemas quadráticos de programação da forma \(\small min(-d^T b + 1/2 \; b^TD \; b)\) com as restrições \(\small A^T b \geq b_0\).

Utilizaremos essa função para a otimização do portfólio.

Duas funções auxiliares 1

pesosCarteira <- function(retornosAtivos, retornoAlvo)
{
## Argumentos:
# retornosAtivos - conjunto de dados dos retornos dos ativos
# retornoAlvo - o retorno-alvo da carteira 
  
##  A função solve.QP() do pacote quadprog implementa o método dual de Goldfarb e Idnani (1982, 1983) 
##  para a solução do problema de otimização quadrática na forma 
##  min(-d'b + 1/2 b' Db) com as restrições A'T b >= b0. 
    
## Para detalhes, veja D. Goldfarb and A. Idnani (1983). "A numerically stable dual method for solving strictly convex
## quadratic programs". Mathematical Programming, 27, 1–33.    
    
## A solução aqui são os pesos que minimizam o risco para o retorno em 'retornoAlvo'    
    
    if(!require("quadprog")) install.packages("quadprog")
    suppressMessages(suppressWarnings(library(quadprog)))
    
    nAtivos  <-  ncol(retornosAtivos)
    portfolio <- solve.QP(
        Dmat <- cov(retornosAtivos),                        # matriz D
        dvec <- rep(0, times = nAtivos),                    # vetor  d
        Amat <- t(rbind(retorno = colMeans(retornosAtivos), # matriz A de restrições
                        orcamento = rep(1, nAtivos), 
                        longa = diag(nAtivos))),
        bvec <- c(retorno = retornoAlvo,                    # vetor  b0
                  orcamento = 1,
                  longa = rep(0, times = nAtivos)),
        meq = 2)                                            # as primeiro meq restrições são igualdades
    
    pesos  <-  portfolio$solution # vetor contendo a solução do problema
    pesos
}

fronteiraCarteira <- function(retornosAtivos, nPontos = 40)
{
    # Quantidade de ativos
    nAtivos <- ncol(retornosAtivos)
    # Retornos-alvo
    mu <- colMeans(retornosAtivos)
    retornoAlvo <- seq(min(mu), max(mu), length = nPontos)
    # Pesos ótimos
    pesos <- rep(0, nAtivos)
    pesos[which.min(mu)] <- 1
    for (i in 2:(nPontos-1)) {
        novosPesos <- pesosCarteira(retornosAtivos, retornoAlvo[i])
        pesos <- rbind(pesos, novosPesos)
    }
    novosPesos <- rep(0, nAtivos)
    novosPesos[which.max(mu)] <- 1
    pesos <- rbind(pesos, novosPesos)
    pesos <- round(pesos, 4)
    colnames(pesos) <- colnames(retornosAtivos)
    rownames(pesos) <- 1:nPontos
    
    # Valor do retorno
    pesos
}

Visualizando todas as carteiras e a fronteira eficiente

retornosAtivos <- ret_log_xts 
names(retornosAtivos)
## [1] "AZUL4.SA" "GGBR4.SA" "PCAR3.SA" "QUAL3.SA" "RENT3.SA"
pesos_front <- fronteiraCarteira(retornosAtivos, nPontos = 200)
as.data.frame(pesos_front)
mu  <-  colMeans(retornosAtivos)
retornoAlvos  <-  seq(min(mu), max(mu), length = nrow(pesos_front))

riscosAlvo  <-  NULL
for (i in 1:nrow(pesos_front)) {
    novoRiscoAlvo  <-  sqrt(pesos_front[i, ] %*% 
                            cov(retornosAtivos) %*%
                            pesos_front[i, ])
    riscosAlvo  <-  c(riscosAlvo, novoRiscoAlvo)
}

fronteiraEficiente <- data.frame(risco = riscosAlvo, retorno = retornoAlvos)

fronteiraEficiente
# Anualizaremos os retornos e os riscos das carteiras na fronteira eficiente
fronteiraEficiente$retorno <- (fronteiraEficiente$retorno+1)^252 - 1
fronteiraEficiente$risco <- fronteiraEficiente$risco * sqrt(252)

cml <- data.frame(x = c(0, max_i_sharpe$Risco),
                  y = c(rf, max_i_sharpe$Retorno))

No gráfico podemos observar todas as quase 5000 carteiras, a fronteira eficiente, a carteira de mínima variância e aquela com o melhor índice Sharpe (carteira de mercado). As ações individuais também estão são apresentadas no espaço risco-retorno. Como mencionado acima, um investidor avesso ao risco exigirá um maior retorno para um determinado nível de risco. Em outras palavras, tentará obter carteiras que residem na fronteira eficiente.


  1. Adaptadas de Basic R for Finance, RMetrics Association & Finance Online Publishing, Capítulo 32↩︎