Avaliando sua carteira: Explorando dados financeiros com R - parte 3

Nesse poste vamos baixar nossa carteira do CEI, baixar as cotações históricas e testar um método de comparação do rendimento diário acumulado com um benchmark.

Saulo Guerra
10-01-2019

Nos dois posts anteriores, primeiro nesse, depois nesse, demos nossos primeiros passos explorando dados financeiros da B3 e do CEI com R. Neste post vamos partir da carteira que baixamos do CEI pro R e tentar calcular o rendimento comparando com um o índice IBOV.

Por ser um mero novato no mercado financeiro, há grande chance de apresentar algum conceito equivocado sobre renda variável. Apesar do objetivo aqui ser explorar o R e as possibilidade de interação com dados da B3, estou completamente aberto para receber dicas, avisos de erros e possíveis correções que precisam ser feitas sobre qualquer coisa que eu venha a escrever a respeito deste tema.

Definindo objetivo

Como objetivo a ser alcançado, baixaremos nossa carteira, baixaremos o histórico de cotações das ações em carteira e calcularemos nosso rendimento, além de comparar com alguns índices.

Achando a fonte de dados

Nossa fonte será o CEI, onde a B3 disponibiliza as informações dos seus ativos financeiros. Usaremos também a API gratuita de cotações do Yahoo.

Vamos usar os seguintes pacotes:


library(rvest)
library(glue)
library(tidyquant)
library(ggplot2)
library(plotly) # para gráficos interativos

Baixando e organizando a carteira

Vamos aproveitar a função apresentada no último post:


consulta_carteira <- function(cpf, senha) {
  url <- 'https://cei.b3.com.br'
  sessao <- html_session(url)
  form_login <- html_form(sessao)
  
  form_preenchido <- set_values(form_login[[1]], 
                                'ctl00$ContentPlaceHolder1$txtLogin' = cpf,
                                'ctl00$ContentPlaceHolder1$txtSenha' = senha)
  
  submit_form(sessao, form_preenchido)
  url_dados <- glue('{url}/CEI_Responsivo/negociacao-de-ativos.aspx')
  pagina_dados <- jump_to(sessao, url_dados)
  instituicoes <- read_html(pagina_dados) %>% 
    html_nodes('option') %>% 
    html_text()
  
  form_dados <- html_form(pagina_dados)
  
  consulta_carteira_banco <- function(i, form_dados) {
      banco <- str_split(i, ' - ', simplify = TRUE)
      numero <- banco[1]
      nome <- banco[2]
      form_dados_preenchidos <- set_values(form_dados[[1]], 
                                           'ctl00$ContentPlaceHolder1$ddlAgentes' = numero)
      
      resultado <- submit_form(sessao, form_dados_preenchidos)
      nao_teve_resultado <- resultado %>%
        read_html() %>% 
        html_text() %>% 
        str_detect('Não foram encontrados resultados para esta pesquisa')
      
      dados_de_um_banco <- data.frame()
      message(glue('lendo banco {nome}'))
      if(!nao_teve_resultado) {
        dados_de_um_banco <- resultado %>% 
          html_nodes('#ctl00_ContentPlaceHolder1_rptAgenteBolsa_ctl00_rptContaBolsa_ctl00_pnAtivosNegociados') %>%
          html_node('table') %>%
          html_table() %>% 
          .[[1]] %>% 
          .[-nrow(.),]
        
        dados_de_um_banco$Nome_banco <- nome
      }
      return(dados_de_um_banco)
  }
  
  # função para converter os textos de valor em números
  converte_texto_em_numero <- function(numero) {
    as.numeric(str_replace_all(str_replace_all(numero, '\\.', ''), ',', '\\.'))
  }
  
  instituicoes <- instituicoes[!grepl("Selecione", instituicoes)] 
  
  dados_totais <- instituicoes %>% 
    map_dfr(~consulta_carteira_banco(.x, form_dados)) %>% 
    mutate_at(.vars = vars(`Valor Total(R$)`, `Preço (R$)`, Quantidade), 
              .funs = list(converte_texto_em_numero)) %>%
    mutate(Data = lubridate::dmy(`Data do Negócio`),
           `Valor Total(R$)` = `Valor Total(R$)`*ifelse(`Compra/Venda`=='V', -1, 1)) %>% 
    select(Data, everything(), -`Data do Negócio`)
    
  return(dados_totais)
}

Fiz algumas modificações na função, mas o intuito é exatamente o mesmo. Caso as alterações não estejam claras ou alguma coisa na função possa ser melhorada, por favor deixe um comentário que esclareceremos.

Carregue a função e baixe sua carteira. Lembre-se de opcionalmente usar o pacote keyring para gerenciar suas senhas


carteira <- consulta_carteira('SEU_CPF', 'SUA_SENHA')

glimpse(carteira)

Observations: 47
Variables: 11
$ Data                     <date> 2019-09-04, 2019-09-04, 2019-09-0…
$ `Compra/Venda`           <chr> "C", "C", "C", "C", "C", "C", "C",…
$ Mercado                  <chr> "Merc. Fracionário", "Merc. Fracio…
$ `Prazo/Vencimento`       <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ `Código Negociação`      <chr> "ITUB3F", "MDIA3F", "MDIA3F", "PSS…
$ `Especificação do Ativo` <chr> "ITAUUNIBANCOON  ED  N1", "M.DIASB…
$ Quantidade               <dbl> 42, 9, 25, 17, 4, 5, 4, 2, 16, 10,…
$ `Preço (R$)`             <dbl> 28.84, 35.50, 35.50, 55.70, 55.70,…
$ `Valor Total(R$)`        <dbl> 1211.28, 319.50, 887.50, 946.90, 2…
$ `Fator de Cotação`       <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ Nome_banco               <chr> "BANCO BTG PACTUAL", "BANCO BTG PA…

Como você pode fazer diferentes compras de uma mesma ação no mesmo dia, para simplificar, vamos pegar o preço médio das ordens agregadas por data. Vamos aproveitar e tirar o F (de Fracionado) do ticker. Só vale a pena manter o F se você for comprar e vender no mercado fracionado, já que os preços de compra e venda costumam ser um pouco diferentes do lote cheio.


carteira <- carteira %>%
  select(everything(), Valor = `Valor Total(R$)`, Preco = `Preço (R$)`, 
         Codigo = `Código Negociação`, Fluxo = `Compra/Venda`) %>% 
  mutate(Codigo = sub('F$', '', Codigo)) %>% 
  group_by(Codigo, Data, Fluxo) %>% 
  summarise(Preco = weighted.mean(Preco, Quantidade),
            Quantidade = sum(Quantidade),
            Total = sum(Valor)) %>% 
  ungroup() %>% 
  arrange(Data) 

Agora vamos preparar uma função para baixar o histórico de cotações da API gratuita do Yahoo. Para isso usaremos a função getSymbol do pacote quantmod, carregado junto com o tidyquant. (O tidyquant é um potente pacote para dados financeiros, que além de agregar os mais importantes pacotes do assunto, apresenta funções sensacionais para gestão de portfólio. Infelizmente ainda não sei nada disso, então o tidyquant está completamente subutilizado neste post).


historico_cotacoes <- function(tickers, inicio = today(), fim = today()) {
  tickers %>% 
    map_dfr(~{
      df <- getSymbols(.x, from = inicio, to = fim,
                       source = 'Yahoo', # fonte: API gratuita do Yahoo
                       auto.assign = FALSE)
      df <- df[,c(4, 6)] 
      # 4 - preço de fechamento
      # 6 - preço ajustado
      names(df) <- c('P_fechamento', 'P_ajustado')
      data.frame(timestamp = index(df), # convertendo em coluna as datas que são indices de linha
                 coredata(df), 
                 symbol = .x, 
                 stringsAsFactors = FALSE)
    }) %>% 
    mutate(symbol = sub('\\.SA$', '', symbol)) %>% # retirando o .SA dos tickers
    select(Data = timestamp, Codigo = symbol, P_fechamento, P_ajustado)
}

Depois de alguns experimentos com a API do Yahoo, percebi um comportamento muito estranho com tickers de Fundo Imobiliário, retornando em alguns casos apenas o último dia. Isso me fez concluir que a API gratuita do Yahoo não parece muito confiável. Caso você conheça alguma API para cotações históricas melhores que a do Yahoo, por favor nos avise nos comentários.

Para não prejudicar o exercício, e até que eu encontre uma API melhor, prosseguiremos sem os tickers terminados em 11.


carteira <- carteira %>% 
  filter(!str_detect(Codigo, '11$')) # $ significa "terminar com" em regex

Recuperemos então as cotações históricas. Por algum motivo estranho, sem explicação aparente, alguns tickers falharam na API do Yahoo passando uma data início que não seja o primeiro dia do mês. Por segurança, vamos resgatar todos históricos desde o primeiro dia do mês da compra mais antiga da carteira. Essa API definitivamente não é boa… mas é gratuita!


inicio <- as.Date(as.yearmon(min(carteira$Data), "%m/%Y"))

cotacoes_historicas <- carteira %>% 
  glue_data('{Codigo}.SA') %>% # glue!
  unique() %>% 
  historico_cotacoes(inicio = inicio, fim = today())

glimpse(cotacoes_historicas)

Observations: 2,100
Variables: 4
$ Data         <date> 2019-03-01, 2019-03-06, 2019-03-07, 2019-03-0…
$ Codigo       <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "ABEV3", "…
$ P_fechamento <dbl> 16.76, 16.37, 16.69, 16.59, 17.11, 17.00, 16.9…
$ P_ajustado   <dbl> 16.76, 16.37, 16.69, 16.59, 17.11, 17.00, 16.9…

head(cotacoes_historicas)

        Data Codigo P_fechamento P_ajustado
1 2019-03-01  ABEV3        16.76      16.76
2 2019-03-06  ABEV3        16.37      16.37
3 2019-03-07  ABEV3        16.69      16.69
4 2019-03-08  ABEV3        16.59      16.59
5 2019-03-11  ABEV3        17.11      17.11
6 2019-03-12  ABEV3        17.00      17.00

tail(cotacoes_historicas)

           Data Codigo P_fechamento P_ajustado
2095 2019-09-27  LINX3        33.20      33.20
2096 2019-09-30  LINX3        32.34      32.34
2097 2019-10-01  LINX3        32.63      32.63
2098 2019-10-02  LINX3        32.43      32.43
2099 2019-10-03  LINX3        33.90      33.90
2100 2019-10-04  LINX3        33.90      33.90

Agora vamos cruzar os dados com a carteira e começar a calcular os rendimentos, por partes.


carteira <- carteira %>% 
  right_join(cotacoes_historicas, by = c('Data', 'Codigo')) %>% # join com cotações
  replace_na(list(Quantidade = 0, Total = 0)) %>% # preenchendo zero nos dias sem movimento
  group_by(Codigo) %>% 
  arrange(Data) %>% 
  mutate(Quantidade_custodia = cumsum(Quantidade), # total acumulado de cada ação
         Valor_mercado = Quantidade_custodia * P_ajustado) %>% # valor de mercado do acumulado no dia
  mutate(Fluxo_dummy = Fluxo, # apenas para me livrar das cotações anteriores as compras
         Fluxo_dummy = na.locf(Fluxo_dummy, na.rm = F)) %>% 
  filter(!is.na(Fluxo_dummy)) %>% 
  select(-Fluxo_dummy) %>% 
  ungroup()

glimpse(carteira)

Observations: 1,676
Variables: 10
$ Codigo              <chr> "ABEV3", "B3SA3", "GRND3", "ITUB3", "PS…
$ Data                <date> 2019-03-06, 2019-03-06, 2019-03-06, 20…
$ Fluxo               <chr> "C", "C", "C", "C", "C", NA, NA, NA, NA…
$ Preco               <dbl> 16.54, 31.74, 8.54, 30.42, 55.84, NA, N…
$ Quantidade          <dbl> 97, 80, 300, 100, 40, 0, 0, 0, 0, 0, 0,…
$ Total               <dbl> 1604.38, 2539.20, 2562.00, 3042.00, 223…
$ P_fechamento        <dbl> 16.37, 31.61, 8.52, 30.49, 55.60, 16.69…
$ P_ajustado          <dbl> 16.370001, 30.963104, 8.288935, 30.1427…
$ Quantidade_custodia <dbl> 97, 80, 300, 100, 40, 97, 80, 300, 100,…
$ Valor_mercado       <dbl> 1587.890, 2477.048, 2486.680, 3014.276,…

Calculando o rendimento

Depois de apanhar um pouco para entender a melhor forma de calcular o rendimento acumulado, de forma agregada, e com a ajuda de ótimas dicas de um amigo, entendi que o ideal é tratar sua carteira inteira como um único fundo de cotas, e suas compras e venda, independente da ação, como compras de cotas desse fundo. Dessa forma, caso queira desagregar para ver o rendimento em diferentes níveis de detalhes ou períodos, basta tratar a agregação desejada como um fundo a parte.

Começaremos sumarizando por dia e pegando a quantidade de ações custodiadas vezes seu respectivo preço de fechamento divido pelo valor total aplicado. Dessa forma teremos o valor custodiado nesse dia. Façamos o mesmo para o preço ajustado, que difere do preço de fechamento pelos ajustes feitos (direto na fonte) para descontar dividendos e outras coisas. Dividido o calculado com valor ajustado pelo calculado com fechamento, pegamos apenas o primeiro dia temos uma cota inicial real.

A partir da cota inicial vamos equalizando a compra e venda de novas cotas.


calcula_cota_inicial <- function(dados) {
  dados %>% 
    filter(Data == min(Data)) %>%  
    group_by(Data) %>% 
    summarise(Valor_cotas = sum((Quantidade * P_fechamento)/sum(Total)),
              Valor_cotas_ajustadas = sum((Quantidade * P_ajustado)/sum(Total)),
              Valor_cotas_real = Valor_cotas_ajustadas/Valor_cotas) %>% 
    pull(Valor_cotas_real)
}

cota_inicial <- carteira %>% 
  calcula_cota_inicial()

Agora calculamos o valor investido (compra e venda).


calcula_valor_investido <- function(dados) {
  dados %>% 
    group_by(Data) %>% 
    summarise(Valor_investido = sum(Total, na.rm = TRUE), 
              Valor_mercado = sum(Valor_mercado, na.rm = TRUE)) %>% 
    mutate(Qtd_cota = Valor_investido,
           Valor_cota = Valor_mercado / Qtd_cota)
}

retorno <- carteira %>% 
  calcula_valor_investido()

Com o valor da cota inicial e com o valor de mercado de cada aporte/venda calculamos o retorno dia a dia apreço de mercado (ajustado) com base na cota inicial e reequilibrando o valor da cota com os novos aportes (a valor de mercado já calculados).


calcula_retorno_cotas <- function(dados) {
  for(i in 2:nrow(dados)) {
    dados$Qtd_cota[i] = dados$Qtd_cota[i-1] + (dados$Valor_investido[i] / dados$Valor_cota[i-1])
    dados$Valor_cota[i] = dados$Valor_mercado[i] / dados$Qtd_cota[i]
  }
  
  return(dados)
}

retorno_cotas <- calcula_retorno_cotas(retorno)

Uma simples função para calcular o retorno em um período agregado. Nesse exercício calculamos por dia mesmo.


mede_retorno_periodo <- function(dados) {
  dados %>% 
    arrange(Data) %>%
    mutate(R_dia = 100 * (Valor_cota/dados$Valor_cota[1]-1))
}

retorno_total <- retorno_cotas %>% 
  mede_retorno_periodo()

Pronto, agora pegamos a série do índice Bovespa para um benchmark básico e aplicamos o mesmo cálculo de retorno/cota. Fazemos um join com o benchmark onde a data é a chave e teremos um data frame com ambas as séries de retornos acumulados, dia a dia, prontas para comparação.


benchmark <- historico_cotacoes('^BVSP', inicio = inicio, fim = today())

retorno_benchmark <- benchmark %>%
  rename(Valor_cota = P_ajustado) %>% 
  filter(Data >= min(retorno_total$Data)) %>% 
  mede_retorno_periodo()

retorno_comparado <- retorno_total %>% 
  select(Data, R_Carteira = R_dia) %>% 
  inner_join(retorno_benchmark %>% 
               select(Data, R_IBOV = R_dia), by='Data')

head(retorno_comparado)

# A tibble: 6 x 3
  Data       R_Carteira R_IBOV
  <date>          <dbl>  <dbl>
1 2019-03-06     0       0    
2 2019-03-07     0.0443  0.131
3 2019-03-08     0.777   1.22 
4 2019-03-11     2.94    4.04 
5 2019-03-12     3.24    3.83 
6 2019-03-13     3.71    4.97 

tail(retorno_comparado)

# A tibble: 6 x 3
  Data       R_Carteira R_IBOV
  <date>          <dbl>  <dbl>
1 2019-09-27      12.1   11.5 
2 2019-09-30      12.7   11.2 
3 2019-10-01      12.1   10.4 
4 2019-10-02       9.41   7.23
5 2019-10-03       9.46   7.75
6 2019-10-04      10.8    8.85

Com um simples gráfico utilizando o plotly + ggplot2 podemos visualizar o comportamento do retorno acumulado comparado com nosso benchmark. Percebe-se que minha carteira (sem os fundos imobiliários!) tem um desempenho lastimável frente ao principal índice de comparação IBOV, sem ganhos relevantes contra o índice, revelando minha falta de talento para comprar ações.


plota_comparacao <- function(dados) {
  dados %>%
    ggplot(aes(x=Data, y=Retorno, group=Serie)) + 
    geom_line(aes(color=Serie)) + 
    geom_hline(yintercept=0) +
    scale_x_date(date_labels = "%b %y", 
                 date_breaks = "1 months",
                 limits = c(min(retorno_comparado$Data), NA)) +
    theme_minimal()
    
}

plot_retorno <- retorno_comparado %>% 
  gather('Serie', 'Retorno', starts_with('R_')) %>% 
  mutate(Retorno = round(Retorno,2)) %>% 
  plota_comparacao() +
  ggtitle('Comparação do retorno acumulado desde o primeiro dia da carteira')

ggplotly(plot_retorno, tooltip = c("Retorno"))

Com poucas adaptações conseguiríamos calcular rendimentos mensais acumulados e inserir mais séries como benchmarnk (CDI, dólar, IFIX, outras carteiras, etc).

Conclusão

Além de ter excluído alguns tickers por problemas na API que usamos como fonte, há uma série de ajustes que precisariam ser feitos nos dados iniciais da carteira para calcular os retornos com mais precisão: tratar desdobramentos, impostos, descontar corretagens na venda, etc.

É necessário encontrar uma API mais confiável do que a do Yahoo, que apresentou comportamento estranho. Seria legal também adaptar a leitura dos dados para flexibilizar um possível input via planilha de Excel ou csv, assim permitira trazer para o R controles de compra e venda já existentes feitos em planilhas fora do CEI.

Em um próximo post vamos tentar explorar mais a fundo a composição da carteira, correlações, alguma avaliação simplificada de riscos e outros tipos de visualizações interessantes para dados financeiros. Vale a pena também um post específico para mostrar o pacote tidyquant.

Citation

For attribution, please cite this work as

Guerra (2019, Oct. 1). Fulljoin: Avaliando sua carteira: Explorando dados financeiros com R - parte 3. Retrieved from https://www.fulljoin.com.br/posts/2019-10-01-b3-rvest-cei-parte-3/

BibTeX citation

@misc{guerra2019avaliando,
  author = {Guerra, Saulo},
  title = {Fulljoin: Avaliando sua carteira: Explorando dados financeiros com R - parte 3},
  url = {https://www.fulljoin.com.br/posts/2019-10-01-b3-rvest-cei-parte-3/},
  year = {2019}
}