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.
- Objetivo
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:
Uma carteira com montante de 1000,00, dividida igualmente entre 4 papéis. Ou seja, 250,00 em cada ou 25%.
Caso um dos ativos dobre de valor, indo para 500,00. Nesse caso, nosso portfólio passa a ter um montante de 1250,00. e agora 1 dos ativos subiu para 40% de representatividade no portfólio. Enquanto, os outros 3 representam 20% cada.
Para ajustar isso, teríamos que vender parte do ativo que subiu e comprar dos outros 3. Para assim, voltarmos ao peso ideal, 25% em cada.
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.