Este estudo analisa a relação entre a taxa SELIC e o comportamento das ações listadas na B3 no período de 2015 a 2022.
Perguntas-chave:
library(readr)
library(dplyr)
library(tidyr)
library(lubridate)
library(ggplot2)
library(zoo)
library(slider)
library(scales)
# Caminhos dos arquivos
stocks_path <- "bovespa_stocks.csv"
macro_path <- "economic_indicators.csv"
# Le arquivos
stocks_raw <- readr::read_csv(stocks_path, show_col_types = FALSE)
macro_raw <- readr::read_csv(macro_path, show_col_types = FALSE)
# Conversão de datas
stocks_raw <- stocks_raw %>% mutate(Date = as.Date(Date))
macro_raw <- macro_raw %>% mutate(Date = as.Date(Date))
# Período: 2015-01-01 a 2022-12-31
start_date <- as.Date("2015-01-01")
end_date <- as.Date("2022-12-31")
stocks_period <- stocks_raw %>% filter(Date >= start_date & Date <= end_date)
# Remover duplicidades
stocks_period <- stocks_period %>% distinct(Symbol, Date, .keep_all = TRUE)
# Filtro 1: Liquidez (Volume médio >= 1,000,000)
liq_stats <- stocks_period %>%
group_by(Symbol) %>%
summarize(mean_vol = mean(Volume, na.rm = TRUE))
keep_liquid <- liq_stats %>% filter(mean_vol >= 1000000) %>% pull(Symbol)
stocks_period <- stocks_period %>% filter(Symbol %in% keep_liquid)
# Filtro 2: Símbolos com exatamente 5 caracteres (ex: ITUB4, PETR4).
# Este filtro ajuda a removermos ativos que nao sao empresas (fundos de investimentos, por exemplo)
stocks_period <- stocks_period %>% filter(nchar(Symbol) == 5)
# Filtro 3: Presença em todos os anos do período
anos_periodo <- seq(lubridate::year(start_date), lubridate::year(end_date))
anos_por_symbol <- stocks_period %>%
mutate(ano = lubridate::year(Date)) %>%
distinct(Symbol, ano) %>%
count(Symbol, name = "anos_com_dados")
full_coverage <- anos_por_symbol %>%
filter(anos_com_dados >= length(anos_periodo)) %>%
pull(Symbol)
stocks_final <- stocks_period %>% filter(Symbol %in% full_coverage)
# Extrair apenas SELIC
macro <- macro_raw %>%
select(Date, selic = `Taxa Selic`) %>%
filter(Date >= start_date & Date <= end_date)
# Forward fill: repetir o valor enquanto nos registros seguintes for nulo
macro <- macro %>%
arrange(Date) %>%
mutate(selic_ff = zoo::na.locf(selic, na.rm = FALSE))
# Variação da SELIC
macro <- macro %>%
mutate(selic_chg = selic_ff - lag(selic_ff))
# Merge diário por Date
final_data <- stocks_final %>%
left_join(macro %>% select(Date, selic_ff, selic_chg),
by = "Date")
# Calcular retornos e volatilidade por Symbol
final_data <- final_data %>%
group_by(Symbol) %>%
arrange(Date, .by_group = TRUE) %>%
mutate(
ret_d = Close / lag(Close) - 1,
vol21 = slider::slide_dbl(ret_d, sd, .before = 20, .complete = TRUE)
) %>%
ungroup()
Dataset de ações
(bovespa_stocks.csv):
| Coluna | Descrição |
|---|---|
Date |
Data da observação (formato YYYY-MM-DD) |
Symbol |
Ticker do ativo |
Open |
Preço de abertura |
High |
Preço máximo do dia |
Low |
Preço mínimo do dia |
Close |
Preço de fechamento |
Adj Close |
Preço de fechamento ajustado (splits, dividendos) |
Volume |
Volume financeiro negociado |
Dataset de indicadores macro
(economic_indicators.csv):
| Coluna | Descrição |
|---|---|
Date |
Data da divulgação/vigência |
Taxa Selic |
Taxa básica de juros (% a.a.) |
IPCA |
Índice de preços ao consumidor amplo (% acumulado) |
IGP-M |
Índice geral de preços - mercado (% acumulado) |
INPC |
Índice nacional de preços ao consumidor (% acumulado) |
Desemprego PNADC |
Taxa de desemprego PNAD Contínua (%) |
# Retorno médio diário agregado
agg_daily <- final_data %>%
group_by(Date) %>%
summarize(ret_mean = mean(ret_d, na.rm = TRUE))
# Caminho acumulado da média dos ativos
market_cum <- agg_daily %>%
arrange(Date) %>%
mutate(mkt_cum = cumprod(1 + coalesce(ret_mean, 0)) - 1)
# Caminho acumulado SELIC (taxa anual -> fator diário)
selic_path <- macro %>%
arrange(Date) %>%
mutate(
selic_daily_ret = (1 + selic_ff/100)^(1/252) - 1,
selic_cum = cumprod(1 + selic_daily_ret) - 1
) %>%
select(Date, selic_cum)
# Juntar
plot_df <- market_cum %>%
left_join(selic_path, by = "Date") %>%
tidyr::pivot_longer(cols = c(mkt_cum, selic_cum), names_to = "Serie", values_to = "ret_cum") %>%
mutate(Serie = dplyr::recode(Serie, mkt_cum = "Média Ativos (igual-ponderada)", selic_cum = "SELIC (100%)"))
ggplot(plot_df, aes(Date, ret_cum, color = Serie)) +
geom_line(size = 1) +
scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
scale_color_manual(values = c("Média Ativos (igual-ponderada)" = "steelblue", "SELIC (100%)" = "firebrick")) +
labs(title = "Rentabilidade acumulada: Média dos ativos filtrados vs investimento 100% SELIC",
subtitle = "Período: 2015–2022",
x = "Data", y = "Retorno acumulado", color = NULL) +
theme_minimal() +
theme(legend.position = "top")
Interpretação:
# Calcular rentabilidade acumulada por ativo
rent_by_asset <- final_data %>%
group_by(Symbol) %>%
summarize(
first_price = first(Close[!is.na(Close)]),
last_price = last(Close[!is.na(Close)]),
ret_cum = (last_price / first_price) - 1,
mean_vol = mean(Volume, na.rm = TRUE)
) %>%
arrange(desc(ret_cum))
# Top 10
top10_rent <- rent_by_asset %>%
slice_head(n = 10) %>%
mutate(pct = ret_cum,
label = paste0(Symbol, "\n", scales::percent(pct, accuracy = 0.1))) %>%
arrange(pct) %>%
mutate(Symbol = factor(Symbol, levels = Symbol))
ggplot(top10_rent, aes(x = "", y = abs(pct), fill = Symbol)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y") +
geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 3.5) +
labs(title = "Top 10 ativos com maior rentabilidade acumulada (2015–2022)",
x = NULL, y = NULL) +
theme_void() +
theme(legend.position = "none")
agg_volume <- final_data %>%
group_by(Date) %>%
summarize(volume_total = sum(Volume, na.rm = TRUE))
ggplot(agg_volume, aes(Date, volume_total)) +
geom_line(color = "darkorange", alpha = 0.8) +
geom_smooth(se = FALSE, color = "black", method = "loess", span = 0.15) +
scale_y_continuous(labels = scales::comma) +
labs(title = "Volume diário total negociado (ativos filtrados)",
subtitle = "Linha preta: tendência suavizada",
x = "Data", y = "Volume total") +
theme_minimal()
ggplot(macro, aes(Date, selic_ff)) +
geom_line(color = "firebrick", size = 1) +
labs(title = "Taxa SELIC ao longo do período (2015–2022)",
x = "Data", y = "SELIC (% a.a.)") +
theme_minimal()
Ciclos identificados:
# Calcular volatilidade por ativo
vol_stats <- final_data %>%
group_by(Symbol) %>%
summarize(
vol_symbol = sd(ret_d, na.rm = TRUE),
first_price = first(Close[!is.na(Close)]),
last_price = last(Close[!is.na(Close)]),
cum_ret = (last_price / first_price) - 1,
n_obs = sum(!is.na(ret_d))
) %>%
filter(n_obs > 50, !is.na(vol_symbol))
if (nrow(vol_stats) > 1) {
median_vol <- median(vol_stats$vol_symbol, na.rm = TRUE)
vol_stats <- vol_stats %>%
mutate(vol_group = if_else(vol_symbol > median_vol, "Mais voláteis", "Menos voláteis"))
# Caminho acumulado por grupo
group_paths <- final_data %>%
semi_join(vol_stats, by = "Symbol") %>%
left_join(vol_stats %>% select(Symbol, vol_group), by = "Symbol") %>%
group_by(vol_group, Date) %>%
summarize(group_ret = mean(ret_d, na.rm = TRUE), .groups = "drop") %>%
arrange(vol_group, Date) %>%
group_by(vol_group) %>%
mutate(group_cum = cumprod(1 + coalesce(group_ret, 0)) - 1) %>%
ungroup()
ggplot(group_paths, aes(Date, group_cum, color = vol_group)) +
geom_line(size = 1) +
scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
scale_color_manual(values = c("Mais voláteis" = "tomato", "Menos voláteis" = "steelblue")) +
labs(title = "Rentabilidade acumulada por grupo de volatilidade",
subtitle = "Grupos definidos pela mediana do desvio-padrão dos retornos diários",
x = "Data", y = "Retorno acumulado", color = "Grupo") +
theme_minimal() +
theme(legend.position = "top")
} else {
cat("Dados insuficientes para grupos de volatilidade.\n")
}
if (exists("vol_stats") && nrow(vol_stats) > 0) {
top10_vol <- vol_stats %>%
arrange(desc(vol_symbol)) %>%
slice_head(n = 10) %>%
mutate(pct = vol_symbol / sum(vol_symbol),
label = paste0(Symbol, " ", scales::percent(pct, accuracy = 0.1))) %>%
arrange(pct) %>%
mutate(Symbol = factor(Symbol, levels = Symbol))
ggplot(top10_vol, aes(x = "", y = pct, fill = Symbol)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y") +
geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 3) +
labs(title = "Top 10 ativos mais voláteis",
x = NULL, y = NULL) +
theme_void() +
theme(legend.position = "none")
}
if (exists("vol_stats") && nrow(vol_stats) > 0) {
bottom10_vol <- vol_stats %>%
arrange(vol_symbol) %>%
slice_head(n = 10) %>%
mutate(pct = vol_symbol / sum(vol_symbol),
label = paste0(Symbol, " ", scales::percent(pct, accuracy = 0.1))) %>%
arrange(pct) %>%
mutate(Symbol = factor(Symbol, levels = Symbol))
ggplot(bottom10_vol, aes(x = "", y = pct, fill = Symbol)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y") +
geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 3) +
labs(title = "Top 10 ativos menos voláteis",
x = NULL, y = NULL) +
theme_void() +
theme(legend.position = "none")
}