Teoria moderna do portfólio

Harry Markowitz, vencedor do Prêmio Nobel de Economia em 1990, é considerado o pioneiro da teoria moderna do portfólio. Com o crescimento das finanças digitais, a relação entre risco e retorno tem sido amplamente discutida. Basicamente, quanto maior o risco, maior é o potencial de retorno de um ativo ou carteira.

Markowitz foi um dos primeiros a integrar modelos econômicos e estatísticos em finanças e investimentos, criando a conhecida fronteira eficiente. Essa fronteira mostra a combinação ideal de ativos em uma carteira, buscando maximizar o retorno para um determinado nível de risco ou minimizar o risco para um determinado retorno. A alocação ótima de ativos é alcançada por meio dessa abordagem.

A teoria moderna do portfólio, fundamentada nos escritos de Markowitz, afirma que os investidores agem de forma racional ao buscar maximizar o retorno com o menor risco possível. Com base nisso, utilizaremos o R para realizar alguns testes de potfólio.

Nosso objetivo aqui será criar dois testes para uma carteira diversificada, um que maximize o retorno e outro que minimize o risco. Irei separar a amostra de teste de alguns ativos escolhidos de maneira arbitrária e usaremos o R para otimizar o peso dos ativos, depois testaremos esses pesos em uma amostra de treino.

Num primeiro momento, vamos carregar os pacotes para análise. Usaremos principalmente as funções do tidyquant, PortfolioAnalytics e tidyverse. Além disso, usaremos os pacotes ROI para realizar alguns cálculos.

library('PortfolioAnalytics')

library('tidyquant')

library('timetk')

library('ROI.plugin.quadprog')

library('ROI.plugin.glpk')

library('tidyverse')

library('ggrepel')

library('knitr')

2. Análise e visualização

Agora, separaremos os ativos e data inicial da análise, será composta por 7 ativos e pegaremos os dados a partir de 2015. Depois usaremos a função tq_get do pacote tidyquant para puxar os preços diretamente o Yahoo Finance:

## tickers utilizados


tickers <- c('BBDC3.SA', 'ABEV3.SA', 'PETR3.SA', 'WEGE3.SA',

             'VALE3.SA', 'TIMS3.SA', 'AGRO3.SA')


inicial_date <- '2015-01-01'


## prices


price_table <-

   tidyquant::tq_get(x = tickers,

                     get = 'stock.prices',

                     from = inicial_date)

Começaremos a realizar a tratativa dos dados, primeiro vamos agrupar os valores por ativo (symbol) e usar a função tq_transmute do tidyquant para mudar os dados de preços que estão como registros diários para mensais. Além disso, usaremos o na.omit para retirar os valores inicias que ficariam zerados depois do grupamento:

## return


return_table <-

  price_table %>%

  group_by(symbol) %>%

  tidyquant::tq_transmute(

    select = adjusted,

    col_rename = 'return',

    mutate_fun = periodReturn,

    period = 'monthly',

    type = 'arithmetic',

    leading = FALSE,

    indexAt = 'lastof'

  ) %>%

  na.omit()

Será realizado o cálculo de retorno acumulado, para isso basta utilizar a função cumsum e plotaremos o gráfico com os resultados por ativos:

## cumulative return graph


cumulative_return <-

  return_table %>%

  group_by(symbol) %>%

  mutate(`cumulative_return` = cumsum(return)) %>%

  select(symbol, date, cumulative_return)


cumulative_return %>% 

  ggplot()+

  aes(x=date, 

      y=cumulative_return*100, 

      color=symbol)+

  geom_line()+

  geom_vline(xintercept = as.Date('2020-03-01'), 

             linetype = 2, 

             color = 'red')+

  geom_hline(yintercept = 0, linetype = 2)+

  facet_wrap(~symbol, scales = 'free_y', ncol=2)

Gráficos feitos apenas para visualização do retorno acumulado. Coloquei uma linha tracejada para mostrar o período da pandemia, março de 2020, que representa quedas significativas na maioria dos papéis e depois plotei utilizando o facet_wrap do ggplot2.

Agora, precisamos realizar a mudança dos dados de long para wide (jogar o nome dos ativos que estão na linha "symbol" para colunas) e repararemos as datas para amostra de treino:

## wide, training


wide_return <-

  return_table %>%

  pivot_wider(names_from = symbol,

              values_from = return) %>%

  column_to_rownames('date') %>%

  as.xts()


wide_return_training <-

  return_table %>%

  pivot_wider(names_from = symbol,

              values_from = return) %>%

  filter(date < as.Date('2021-08-01')) %>%

  column_to_rownames('date') %>%

  as.xts()

3. Treino

Agora que temos tudo pronto, começaremos a utilizar o pacote PortfolioAnalytics para separar os portfolios de teste. Primeiro o portfolio que minimiza o risco. 

Basicamente, queremos que o R nos retorne qual seria a combinação perfeita de pesos para cada ativo dentro da carteira que nos retorne o mínimo de risco possível, dentro da medida de dispersão que decidirmos utilizar, no caso, variância. Dessa forma, usaremos a função portfolio.spec:

## minimum variance portfolio


min_var_portfolio <-

  portfolio.spec(assets = tickers) %>% #selecao dos ativos

  add.constraint(type = 'full_investment') %>%   #definindo as restricoes do portfolio

  add.constraint(type = 'box',                   #tipo que seleciona valores minimos e maximos para cada ativo

                 min = 0.05,                     #percentual minimo de um ativo na carteira é de 5%

                 max = 0.60) %>%                 #percentual maximo de um ativo na carteira é de 60%

  add.objective(type = 'risk',                   #variavel que queremos otimizar, no caso, risco

                name = 'var')                    #medida que usaremos no risco, no caso, variancia

  

optimization_risk <- 

  wide_return_training %>%

  optimize.portfolio(portfolio = min_var_portfolio,      #otimizacao do portfolio em si

                     optimize_method = 'quadprog',       #metodo utilizado, nesse caso do pacote Rglpk

                     trace = TRUE)


optimization_risk

weight_risk <- extractWeights(optimization_risk) #extrai apenas o peso do objeto

> optimization_risk

***********************************

PortfolioAnalytics Optimization

***********************************


Call:

optimize.portfolio(R = ., portfolio = min_var_portfolio, optimize_method = "quadprog", 

    trace = TRUE)


Optimal Weights:

BBDC3.SA ABEV3.SA PETR3.SA WEGE3.SA VALE3.SA TIMS3.SA AGRO3.SA 

  0.0500   0.2932   0.0500   0.2359   0.0500   0.0500   0.2708 


Objective Measure:

StdDev 

0.0531

Faremos a mesma coisa para o referencial de retorno máximo, ou seja, qual peso que cada ativo deve ter para termos o máximo de retorno dessa carteira:

## maximum return portfolio


max_return_portfolio <-

  portfolio.spec(assets = tickers) %>%

  add.constraint(type = 'full_investment') %>%

  add.constraint(type = 'box', 

                 min = 0.05, 

                 max = 0.60) %>%

  add.objective(type = 'return', 

                name = 'mean')


optimal_return <-

  wide_return_training %>%

  optimize.portfolio(portfolio = max_return_portfolio,

                     optimize_method = 'glpk',

                     trace = TRUE)

optimal_return

weight_return <- extractWeights(optimal_return)

> optimal_return

***********************************

PortfolioAnalytics Optimization

***********************************


Call:

optimize.portfolio(R = ., portfolio = max_return_portfolio, optimize_method = "glpk", 

    trace = TRUE)


Optimal Weights:

BBDC3.SA ABEV3.SA PETR3.SA WEGE3.SA VALE3.SA TIMS3.SA AGRO3.SA 

    0.05     0.05     0.15     0.05     0.60     0.05     0.05 


Objective Measure:

   mean 

0.02801

Agora vamos unir em um tibble os objetos weights para retorno máximo e risco mínimo, depois plotaremos um gráfico para exibição:

## optimal portfolio weights


tibble('symbol' = tickers,

       'weight_return' = weight_return,

       'weight_risk' = weight_risk) %>%

  pivot_longer(cols = 'weight_return':'weight_risk') %>%

  ggplot(aes(x = name, 

             y = value, 

             fill = symbol))+

  geom_col()+

  scale_x_discrete(labels = c('Max return', 

                              'Min risk'))+

  geom_text(aes(label = symbol),

            position = position_stack(vjust = .6),

            size = 2)+

  labs(x='',

       y='')

Agora que realizamos a separação de otimização dos ativos nos dados de treino, podemos ver que desempenho as carteiras teriam nos dados de teste. Ou seja, veremos qual seria o retorno acumulado das nossas carteiras caso tivéssemos ficado com elas após a data de treino (2021-08-01) até hoje (2023-05-01).

Então, primeiro iremos refazer as tabelas com o filtro que desejamos e realizar a união:

4. Teste

## test return


max_return_portfolio_test <- 

  return_table %>%

  filter(date > as.Date('2021-08-01')) %>%

  tq_portfolio(

    assets_col = symbol,

    returns_col = return,

    weights = weight_return,

    rebalance_on = 'months'

  )


min_risk_portfolio_test <- 

  return_table %>%

  filter(date > as.Date('2021-08-01')) %>%

  tq_portfolio(

    assets_col = symbol,

    returns_col = return,

    weights = weight_risk,

    rebalance_on = 'months'

  )


## total


total_return <-

  max_return_portfolio_test %>%

  left_join(min_risk_portfolio_test, by='date')

Agora vamos realizar o tratamento dessa tabela e a visualização dela, vou deixar a explicação nos comentários do código:

cumulative_return_longer <-

  total_return %>%

  rename(max_return_portfolio_test = portfolio.returns.x,         #troca os nomes das colunas

         min_risk_portfolio_test = portfolio.returns.y) %>%

  pivot_longer(-date,                                             #exclui a coluna date e faz um alongamento da tabela

               names_to = 'type',

               values_to = 'return') %>%

  group_by(type) %>%                                              #agrupa pela variavel tipo (ativo)

  mutate('cumulative_return' = cumsum(return)) %>%                #cria a coluna com o retorno acumulado

  ungroup()


cumulative_return_longer %>%

  ggplot()+

  aes(x=date, y=`cumulative_return`*100, color=`type`)+

  geom_line(size=1.2)+

  facet_wrap(~type)+

  geom_label_repel(mapping = aes(y = `cumulative_return`*100,

                                 label = format(round(`cumulative_return`*100, 2),

                                                big.mark = ',',

                                                decimal.mark = '.',

                                                nsmall = 2)),

                   data = filter(drop_na(cumulative_return_longer, cumulative_return),

                                         date == max(date)),

                   nudge_x = 0.1,

                   color='white',

                   fill='black',

                   fontface='bold',

                   label.size = 0)+

  labs(x='',

       y='')+

  theme(legend.position = 'none')

No período analisado, baseado na otimização das carteiras, a estratégia que maximizava o risco teve um retorno de 6.40% enquanto a carteira que minimizou o risco, teve um retorno de 15.38%. Vou colocar a tabela abaixo com os retornos caso tivéssemos investido 1000 reais em cada uma das carteiras:

## cumulative return by portfolio type


cumulative_return_longer %>%

  select(-return) %>%

  filter(date == max(date)) %>%

  mutate(return_real = 1000 + (1000*`cumulative_return`)) %>%

  knitr::kable(align = 'c')

> cumulative_return_longer %>%

+  select(-return) %>%

+  filter(date == max(date)) %>%

+  mutate(return_real = 1000 + (1000*`cumulative_return`)) %>%

+  knitr::kable(align = 'c')


|    date    |           type            | cumulative_return | return_real |

|:----------:|:-------------------------:|:-----------------:|:-----------:|

| 2023-05-31 | max_return_portfolio_test |     0.0640475     |  1064.048   |

| 2023-05-31 |  min_risk_portfolio_test  |     0.1538256     |  1153.826   |

Assim, se tivéssemos investido 1000,00 no portfólio que procurava maximizar o retorno, estaríamos com 1064,05 reais em 2023-05-31. Já, se tivéssemos investido a mesma quantia no portfólio que procurava minimizar o risco teríamos um montante de 1153.83 reais na mesma data.

5. Rebalanceamento

Como uma medida interessante de análise, podemos configurar um rebalanceamento automático usando outra função do pacote PortfolioAnalytics, chamada optimize.portfolio.rebalancing para que assim nossa carteira mantenha seus níveis de pesos ideais. Aqui vai um exemplo, se temos:

Faremos isso com nossas duas carteiras e veremos quanto ficariam seus retornos acumulados no mesmo período de teste anterior. 

Primeiro, vamos realizar a manipulação dos dados: 

## risk minimization with rebalancing


rebalance_risk <- 

  wide_return %>%

  optimize.portfolio.rebalancing(

    optimize_method = 'ROI',

    portfolio = min_var_portfolio,

    rebalance_on = 'quarters',            #PERÍODO DE REBALANCEAMENTO 

    training_period = 60,

    rolling_window = 60)


extractWeights(rebalance_risk) %>%

  as.data.frame() %>%

  rownames_to_column('date') %>%

  mutate(date = as.Date(date)) %>%

  pivot_longer(cols = 'BBDC3.SA':'AGRO3.SA') %>%

  ggplot()+

  aes(x=date, y=value, fill=name)+

  geom_col()+

  scale_x_date(breaks = '6 months')



## return maximization with rebalancing


rebalance_return <- 

  wide_return %>%

  optimize.portfolio.rebalancing(

    optimize_method = 'ROI',

    portfolio = max_return_portfolio,

    rebalance_on = 'quarters',

    training_period = 60,

    rolling_window = 60)


extractWeights(rebalance_return) %>%

  as.data.frame() %>%

  rownames_to_column('date') %>%

  mutate(date = as.Date(date)) %>%

  pivot_longer(cols = 'BBDC3.SA':'AGRO3.SA') %>%

  ggplot()+

  aes(x=date, y=value, fill=name)+

  geom_col()+

  scale_x_date(breaks = '6 months')

Vamos unir as variáveis em um dataframe e criar um objeto long para a visualização gráfica:

## comparacao de performance


r1 <- 

  wide_return %>%

  Return.portfolio(weights = extractWeights(rebalance_risk)) %>%

  as.data.frame() %>%

  rename(risk_min = 1)


r2 <-

  wide_return %>%

  Return.portfolio(weights = extractWeights(rebalance_return)) %>%

  as.data.frame() %>%

  rename(return_max = 1)


total_r <-

  cbind(r1, r2)


## graph


total_r_long <-

  total_r %>%

  rownames_to_column('date') %>%

  pivot_longer(cols = 2:3) %>%

  mutate(date = as.Date(date)) %>%

  group_by(name) %>%

  mutate(cumulative_return = cumsum(value))

Agora vamos plotar a visualização dos retorno acumulado:

total_r_long %>% 

  ggplot()+

  aes(x=date, 

      y=cumulative_return*100, 

      color = name)+

  geom_line(size=1.3)+

  facet_wrap(~name)+

  geom_label_repel(mapping = aes(y = `cumulative_return`*100,

                                 label = format(round(`cumulative_return`*100, 2),

                                                big.mark = ',',

                                                decimal.mark = '.',

                                                nsmall = 2)),

                   data = filter(drop_na(total_r_long, cumulative_return),

                                 date == max(date)),

                   nudge_x = 0.1,

                   color='white',

                   fill='black',

                   fontface='bold',

                   label.size = 0)+

  labs(x='',

       y='')+

  theme(legend.position = 'none')

Como podemos ver o retorno é muito maior realizando um rebalanceamento a cada 4 meses, uma vez que otimiza mais a proporção perfeita dos ativos. E diminui ainda mais o risco, nesse exemplo, nosso portfólio que maximiza o retorno teve um acumulado de 81.27%, enquanto o que minimiza o risco teve 71.82%.

6. Conclusão

Como podemos ver é possível utilizar matemática e estatística, com o uso das ferramentas corretas (no caso usamos o R). Para chegarmos a conclusões interessantes. É de conhecimento do mercado que quanto maior o retorno esperado, maior será o risco, porém, também é de conhecimento que retorno passado não é garantia de retorno futuro. 

No exemplo que utilizamos é interessante notar que a opção que buscava diminuir o risco, teve um retorno maior no teste do que aquela que buscava maximizar o retorno. Como nosso teste foi realizado baseado no último ano, que vimos um aumento significativo do nível de juros, que faz com que ocorra uma fuga de capital para ativos com menor nível de risco, já que com o aumento da Selic o prêmio pelo risco diminui.

Agora quando olhamos para o item 5, podemos ver que o rebalanceamento potencializa a relação risco x retorno, já que diminuímos a posição em ativos que subiram de preço e realocamos nos demais ativos da carteira, mantendo a estratégia perfeita de alocação que identificamos na amostra de treino. Porém, isso não é uma verdade absoluta, como já mencionado anteriormente retorno passado não é garantia de retorno futuro.

Apesar de nada no mercado ser completamente exato e com possibilidades de previsão, porém, as finanças quantitativas ganham cada vez mais força e combinar esse tipo de análise com outros métodos de estatística e machine learning pode gerar insigths poderosos.

7. Referências