Construção de Portfólio de Ações#
Estratégia para Refletir as Principais Participações em Setores do Ibovespa#
Resumo#
Este artigo propõe a criação de uma carteira fictícia como parte integrante de um estudo subsequente de análise de risco de portfólio de ações, composta por 10 ativos diversos. A abordagem adotada consiste na seleção da maior participação em cada setor, com a determinação dos pesos baseada na participação percentual do papel que compõe o índice Bovespa. Para tanto, utilizou-se a tabela
📌 Nota: Este portfólio de ações será utilizado em estudos futuros para simular a análise de riscos em carteiras de investimentos. A metodologia ou os resultados obtido não devem ser considerados como recomendação de compra ou venda de ativos.
Índice Bovespa (Ibovespa B3)#
O Ibovespa é uma carteira teórica de ativos composta por ações e units de empresas que atendem a critérios específicos, representando aproximadamente 80% dos negócios e do volume financeiro no mercado de capitais brasileiro. Criado em 1968, o índice tornou-se uma referência global ao longo de 50 anos sendo o principal indicador de desempenho das ações na B3, reunindo as empresas mais importantes do mercado brasileiro.
Descrição e coleta dos dados#
Importação das bibliotecas que serão utilizadas:
import pandas as pd
import numpy as np
Lendo os dados da Carteira Teórica do IBovespa:
csv = './data/IBOVDia_12-01-24.csv'
ibov = pd.read_csv(csv,
sep=';',
encoding='ISO-8859-1',
skipfooter=2,
engine='python',
thousands='.',
decimal=',',
header=1,
index_col=False)
ibov.head()
Setor | Código | Ação | Tipo | Qtde. Teórica | Part. (%) | Part. (%)Acum. | |
---|---|---|---|---|---|---|---|
0 | Bens Indls / Máqs e Equips | WEGE3 | WEG | ON NM | 1481593024 | 2.316 | 2.316 |
1 | Bens Indls / Mat Transporte | EMBR3 | EMBRAER | ON NM | 734632705 | 0.722 | 0.722 |
2 | Bens Indls/Transporte | AZUL4 | AZUL | PN N2 | 327593725 | 0.209 | 2.157 |
3 | Bens Indls/Transporte | CCRO3 | CCR SA | ON NM | 995335937 | 0.613 | 2.157 |
4 | Bens Indls/Transporte | GOLL4 | GOL | PN N2 | 198184909 | 0.071 | 2.157 |
Primeiramente, é fundamental obtermos uma visão abrangente dos rótulos das colunas, examinarmos os tipos de dados presentes e checarmos se há valores ausentes em nossa base.
ibov.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87 entries, 0 to 86
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Setor 87 non-null object
1 Código 87 non-null object
2 Ação 87 non-null object
3 Tipo 87 non-null object
4 Qtde. Teórica 87 non-null int64
5 Part. (%) 87 non-null float64
6 Part. (%)Acum. 87 non-null float64
dtypes: float64(2), int64(1), object(4)
memory usage: 4.9+ KB
Para o entendimento dos dados, os metadados da tabela fornecerão informações sobre o significado de cada coluna.
Colunas:
Setor: (Tipo: Texto) - O setor e subsetor ao qual a ação pertence (separados por "/").
Código: (Tipo: Texto) - O código identificador único usado na bolsa de valores (B3) para identificar e negociar um determinado ativo.
Ação: (Tipo: Texto) - O nome da ação.
Tipo: (Tipo: Texto) - O tipo de ação seguido pelo Nivel de Governança (separados por " ").
Qtde. Teórica: (Tipo: Numérico) - A quantidade teórica de ações no índice Bovespa.
Part. (%): (Tipo: Numérico) - A participação percentual da ação no índice Bovespa.
Part. (%)Acum.: (Tipo: Numérico) - A participação percentual do Setor / SubSetor da ação no índice Bovespa.
Conforme observado nos metadados, a coluna “Setor” apresenta o setor e subsetor da ação separados por “/”. Podemos identificá-los utilizando a função unique()
.
ibov['Setor'].unique()
array(['Bens Indls / Máqs e Equips', 'Bens Indls / Mat Transporte',
'Bens Indls/Transporte', 'Cons N Básico / Alimentos Processados',
'Cons N Cíclico / Bebidas', 'Cons N Cíclico / Comércio Distr.',
'Cons N Cíclico / Pr Pessoal Limp', 'Cons N Ciclico/Agropecuária',
'Consumo Cíclico / Comércio', 'Consumo Cíclico / Tecid Vest Calç',
'Consumo Cíclico/Constr Civil', 'Consumo Cíclico/Viagens e Lazer',
'Diversos', 'Financ e Outros / Explor Imóveis',
'Financ e Outros / Holdings Divers',
'Financ e Outros / Interms Financs',
'Financ e Outros / Previd Seguros',
'Financeiro e Outros/Serviços Financeiros Diversos',
'Mats Básicos / Madeira e Papel', 'Mats Básicos / Mineração',
'Mats Básicos / Químicos', 'Mats Básicos / Sid Metalurgia',
'Petróleo/ Gás e Biocombustíveis', 'Saúde/Comércio Distr.',
'Saúde/SM Hosp An.Diag', 'Tec.Informação/Programas Servs',
'Telecomunicação', 'Utilidade Públ / Água Saneamento',
'Utilidade Públ / Energ Elétrica'], dtype=object)
Vamos separar os dados desta coluna em “Setor” e “Subsetor”.
# Separando os dados antes e depois da "/".
# O parâmetro 'expand=True' retorna os dados em colunas separadas.
ibov[['Setor', 'Subsetor']] = ibov['Setor'].str.split('/', expand=True)
# Removendo os espaços extras ao redor das strings
ibov['Setor'] = ibov['Setor'].str.strip()
ibov['Subsetor'] = ibov['Subsetor'].str.strip()
Agora, as informações de Setor e Subsetor das ações estão separadas em colunas distintas.
ibov['Setor'].unique()
array(['Bens Indls', 'Cons N Básico', 'Cons N Cíclico', 'Cons N Ciclico',
'Consumo Cíclico', 'Diversos', 'Financ e Outros',
'Financeiro e Outros', 'Mats Básicos', 'Petróleo', 'Saúde',
'Tec.Informação', 'Telecomunicação', 'Utilidade Públ'],
dtype=object)
ibov['Subsetor'].unique()
array(['Máqs e Equips', 'Mat Transporte', 'Transporte',
'Alimentos Processados', 'Bebidas', 'Comércio Distr.',
'Pr Pessoal Limp', 'Agropecuária', 'Comércio', 'Tecid Vest Calç',
'Constr Civil', 'Viagens e Lazer', None, 'Explor Imóveis',
'Holdings Divers', 'Interms Financs', 'Previd Seguros',
'Serviços Financeiros Diversos', 'Madeira e Papel', 'Mineração',
'Químicos', 'Sid Metalurgia', 'Gás e Biocombustíveis',
'SM Hosp An.Diag', 'Programas Servs', 'Água Saneamento',
'Energ Elétrica'], dtype=object)
Novamente, iremos verificar se as novas colunas possuem dados faltantes.
ibov[['Setor', 'Subsetor']].info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87 entries, 0 to 86
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Setor 87 non-null object
1 Subsetor 81 non-null object
dtypes: object(2)
memory usage: 1.5+ KB
Observa-se que existem 6 linhas com valores nulos na coluna “Subsetor”. Podemos identificá-las utilizando o método .isna()
:
ibov[ibov['Subsetor'].isna()]
Setor | Código | Ação | Tipo | Qtde. Teórica | Part. (%) | Part. (%)Acum. | Subsetor | |
---|---|---|---|---|---|---|---|---|
28 | Diversos | COGN3 | COGNA ON | ON NM | 1814920980 | 0.269 | 3.056 | None |
29 | Diversos | RENT3 | LOCALIZA | ON NM | 853202347 | 2.337 | 3.056 | None |
30 | Diversos | VAMO3 | VAMOS | ON NM | 421383330 | 0.180 | 3.056 | None |
31 | Diversos | YDUQ3 | YDUQS PART | ON NM | 289347914 | 0.270 | 3.056 | None |
73 | Telecomunicação | VIVT3 | TELEF BRASIL | ON | 423091712 | 0.956 | 1.574 | None |
74 | Telecomunicação | TIMS3 | TIM | ON NM | 807896814 | 0.618 | 1.574 | None |
Substituiremos os dados faltantes do Subsetor pelos mesmos do Setor.
ibov['Subsetor'] = ibov['Subsetor'].fillna(ibov['Setor'])
ibov.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87 entries, 0 to 86
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Setor 87 non-null object
1 Código 87 non-null object
2 Ação 87 non-null object
3 Tipo 87 non-null object
4 Qtde. Teórica 87 non-null int64
5 Part. (%) 87 non-null float64
6 Part. (%)Acum. 87 non-null float64
7 Subsetor 87 non-null object
dtypes: float64(2), int64(1), object(5)
memory usage: 5.6+ KB
Entendemos que alguns setores podem ser agrupados. Portanto, vamos consolidá-los para formar 10 setores distintos.
def agrupa_setor(setor):
if setor in ['Cons N Básico', 'Cons N Cíclico', 'Cons N Ciclico']: return "Consumo Não-Cíclico"
if setor in ['Financ e Outros', 'Financeiro e Outros']: return "Financeiro"
if setor in ['Tec.Informação', 'Telecomunicação']: return "TI e Telecom"
else: return setor
ibov['Setor'] = ibov['Setor'].apply(agrupa_setor)
ibov['Setor'].unique()
array(['Bens Indls', 'Consumo Não-Cíclico', 'Consumo Cíclico', 'Diversos',
'Financeiro', 'Mats Básicos', 'Petróleo', 'Saúde', 'TI e Telecom',
'Utilidade Públ'], dtype=object)
Critérios de Seleção#
As ações escolhidas serão aquelas que ocupam a maior participação em cada setor. Para isso, ordenou-se as ações por participação na ordem decrescente e selecionou-se a primeira de cada setor.
# Ordenando as ações por participação na ordem decrescente e selecionando a primeira de cada setor.
Carteira = ibov.sort_values(by=['Part. (%)'], ascending=False).groupby(['Setor']).head(1)
Carteira
Setor | Código | Ação | Tipo | Qtde. Teórica | Part. (%) | Part. (%)Acum. | Subsetor | |
---|---|---|---|---|---|---|---|---|
51 | Mats Básicos | VALE3 | VALE | ON NM | 4196924316 | 13.681 | 14.311 | Mineração |
60 | Petróleo | PETR4 | PETROBRAS | PN N2 | 4566445852 | 7.805 | 17.879 | Gás e Biocombustíveis |
40 | Financeiro | ITUB4 | ITAUUNIBANCO | PN N1 | 4801593832 | 7.189 | 17.747 | Interms Financs |
79 | Utilidade Públ | ELET3 | ELETROBRAS | ON N1 | 1980568384 | 3.843 | 10.954 | Energ Elétrica |
11 | Consumo Não-Cíclico | ABEV3 | AMBEV S/A | ON | 4394245879 | 2.675 | 2.675 | Bebidas |
29 | Diversos | RENT3 | LOCALIZA | ON NM | 853202347 | 2.337 | 3.056 | Diversos |
0 | Bens Indls | WEGE3 | WEG | ON NM | 1481593024 | 2.316 | 2.316 | Máqs e Equips |
67 | Saúde | RADL3 | RAIADROGASIL | ON NM | 1275798515 | 1.614 | 2.248 | Comércio Distr. |
73 | TI e Telecom | VIVT3 | TELEF BRASIL | ON | 423091712 | 0.956 | 1.574 | Telecomunicação |
20 | Consumo Cíclico | LREN3 | LOJAS RENNER | ON NM | 951329770 | 0.703 | 1.431 | Comércio |
Ponderação#
Determinou-se os pesos das ação pela sua participação no Ibovespa. Ao adotar essa abordagem de ponderação, buscou-se assegurar que a composição da carteira reflita não apenas a posição no setor, mas também a influência da ação no índice como um indicador mais abrangente do mercado.
#Distribuindo os pesos na carteira
Carteira['Peso'] = Carteira['Part. (%)'] / Carteira['Part. (%)'].sum()
#Selecionando somente as colunas necessárias.
Carteira = Carteira[['Código', 'Ação', 'Peso', 'Setor', 'Subsetor']].reset_index(drop=True)
Carteira
Código | Ação | Peso | Setor | Subsetor | |
---|---|---|---|---|---|
0 | VALE3 | VALE | 0.317285 | Mats Básicos | Mineração |
1 | PETR4 | PETROBRAS | 0.181011 | Petróleo | Gás e Biocombustíveis |
2 | ITUB4 | ITAUUNIBANCO | 0.166725 | Financeiro | Interms Financs |
3 | ELET3 | ELETROBRAS | 0.089125 | Utilidade Públ | Energ Elétrica |
4 | ABEV3 | AMBEV S/A | 0.062038 | Consumo Não-Cíclico | Bebidas |
5 | RENT3 | LOCALIZA | 0.054199 | Diversos | Diversos |
6 | WEGE3 | WEG | 0.053712 | Bens Indls | Máqs e Equips |
7 | RADL3 | RAIADROGASIL | 0.037431 | Saúde | Comércio Distr. |
8 | VIVT3 | TELEF BRASIL | 0.022171 | TI e Telecom | Telecomunicação |
9 | LREN3 | LOJAS RENNER | 0.016304 | Consumo Cíclico | Comércio |
#Salvando os dados da carteira em formato .xlsx com pandas
Carteira.to_excel('./data/carteira.xlsx', index = False)
Conclusão#
Por fim, temos uma carteira que tenta refletir as principais posições nos setores do Ibovespa na data 12-01-2024, com pesos proporcionais à participação de cada ação no índice e contando com informações sobre o setor, subsetor e nome da ação, os quais serão utilizadas em análises futuras.