Técnicas de Machine Learning estão na moda em diversos setores e indústrias, por isso não poderíamos deixar de pesquisar como aplicar essas técnicas como ferramentas de auxílio nas tomadas de decisões de investimentos e trading. O que são exatamente, como funcionam e os potenciais usos, são assuntos para um próximo post.
Neste post, eu quero trazer algumas técnicas (relativamente) simples, mas muito robustas e com aplicação direta tanto para o pequeno investidor, quanto para o trader institucional.
Este post inclui um código em Python para o leitor acompanhar e, se quiser, modificar e fazer suas próprias análises (se você nunca utilizou o Notebook Python, basta clicar na primeira célula e ir apertando Shift+Enter para rodar cada célula. É importante rodá-las em ordem). Inclusive, este post pode ser lido com vários olhares, o curioso pode apenas acompanhar o texto e os resultados, o matemático pode ir atrás para entender melhor cada técnica utilizada e o programador pode pensar em como implementar tudo isso de forma mais eficiente.
Vamos Lá
Vamos aplicar técnicas de Machine Learning na matriz de correlação de ações do mercado brasileiro e ver quais insights conseguimos extrair. A implementação foi baseada em [1]. Mas antes de tudo, vamos atrás dos dados. Para baixá-los, utilizei a biblioteca yfinance que usa as informações do Yahoo Finance:
## Ativos escolhidos e período selecionado
ativos = ['ITUB4.SA', 'PETR4.SA', 'BBDC4.SA', 'PETR3.SA', 'ABEV3.SA', 'MGLU3.SA', 'WEGE3.SA', 'BBAS3.SA', \
'ITSA4.SA', 'JBSS3.SA', 'VALE3.SA', 'SUZB3.SA', 'NTCO3.SA', 'LREN3.SA', 'RENT3.SA', \
'BBDC3.SA', 'EQTL3.SA', 'RAIL3.SA', 'RADL3.SA', 'UGPA3.SA', 'BPAC11.SA', 'SBSP3.SA', 'GGBR4.SA', \
'BBSE3.SA', 'BRFS3.SA', 'KLBN11.SA', 'CCRO3.SA', 'HAPV3.SA', 'HYPE3.SA', 'SULA11.SA', 'COGN3.SA', 'CSAN3.SA', \
'ELET3.SA', 'ENGI11.SA', 'TOTS3.SA', 'EGIE3.SA', 'SANB11.SA', 'CMIG4.SA', 'PCAR3.SA', 'BRAP4.SA', 'IRBR3.SA', \
'YDUQ3.SA', 'CSNA3.SA', 'BRML3.SA', 'ELET6.SA', 'QUAL3.SA', 'CRFB3.SA', 'FLRY3.SA', 'AZUL4.SA', 'CYRE3.SA', 'MRFG3.SA', \
'TAEE11.SA', 'BRKM5.SA', 'MULT3.SA', 'EMBR3.SA', 'CIEL3.SA', 'GOAU4.SA', 'MRVE3.SA', 'ENBR3.SA', 'CPFE3.SA', 'USIM5.SA', \
'BEEF3.SA', 'CVCB3.SA', 'GOLL4.SA', 'ECOR3.SA']
dataInicial = '2019-01-01'
dataFinal = '2022-08-01'
data = yf.download(ativos, dataInicial, dataFinal)['Close'] # Baixar os dados de preço
data.dropna(1, inplace= True) # Remove os ativos sem dados
data # Visualizar os preços
# Cria a lista de retornos de treino e teste
retData_train = data.pct_change()[1:int(len(data)*0.6)]
retData_test = data.pct_change()[int(len(data)*0.6):]
Dividimos os dados de retorno das ações em dados de treino (03/01/2019 a 25/02/2021), onde iremos aplicar as técnicas e dados de teste (26/02/2021 a 29/07/2022), onde testamos se o resultado da análise funciona no futuro. Essa divisão é muito importante para evitar o overfitting, que é criar modelos que só funcionam naquele exato período que olhamos e são ruins quando aplicados na realidade.
Matriz de Correlação e Dendrograma
Relembrando, a matriz de correlação é uma forma de representar a correlação de cada par de ação. Vamos dar uma olhada na matriz dos nossos dados. Como temos mais de 60 ativos, olhar tudo em uma matriz é difícil, a representação por heatmap pode ajudar. Aqui, quanto mais claro o ponto, maior a correlação entre dois ativos e quanto mais escuro, menor.

Repare que acima e à direita (são a mesma coisa) da imagem há linhas conectando cada par de ativos, como chaves de um campeonato. Esse diagrama é chamado de Dendrograma. O Dendrograma usa a similaridade entre cada objeto (no caso a similaridade é medida pela correlação) para criar uma hierarquia. Dois objetos mais próximos entre si ficam na mesma chave, o próximo objeto mais próximo desses dois se junta a essa chave, duas chaves podem se juntar e por aí vai. Quando mais alta a linha que conecta dois objetos ou chaves menores, mais diferentes entre si eles são. Na imagem abaixo, fica mais fácil de visualizar:

Repare como PETR3 e PETR4 estão conectadas por uma chave bem baixa, enquanto que para você sair de MGLU3 e chegar em SUZB3 vai precisar percorrer um longo caminho. Os dendrogramas são muito úteis para agrupar e categorizar as coisas, como setores e indústrias, no caso de empresas. Mas repare, lá em cima ele junta SUZB3 e KLBN11 (ambas de celulose) de forma muito distante, enquanto conecta WEGE3 (motores elétricos) e TOTS3 (software) de forma mais próxima. Estranho, não?
Isso ocorre porque a matriz de correlação que extraímos dos dados é ruidosa, é suja por sinais que não são informações. WEGE3 e TOTS3 ficaram próximas porque, por pura coincidência, andaram mais juntas do que o normal. Se pegarmos outro período, as conexões vão ser diferentes. Para “limparmos” vamos utilizar uma técnica de Machine Learning (ou estatística, para os puristas) muito popular chamada de Análise de Componente Principal (Principal Componente Analysis, PCA).
PCA
No PCA, fazemos a matriz de correlação (C) ser reescrita como a multiplicação de três matrizes:

W é a matriz de autovetores, ∧ é a matriz de autovalores (com valores apenas na diagonal) e W^T é a matriz W transposta (o que é linha vira coluna e vice-versa). Se você estudou engenharia, deve estar relembrando das noites mal dormidas estudando autovalores e autovetores. Se você nunca ouviu esses termos, não se preocupe. De maneira bem resumida: transformamos o retorno de cada ação numa regressão linear, uma soma de vários fatores (esses fatores são puramente estatísticos não tendo necessariamente uma interpretação econômica). Esses fatores tem correlação 0 entre si (o que é matematicamente útil). Cada valor da matriz de autovetores representa o peso de cada fator para cada uma das ações. Os valores da matriz de autovalores representam a “variabilidade” de cada fator, um fator com um autovalor mais alto é responsável por uma variação maior nos preços das ações.
De novo, se você nunca viu esses termos, não se preocupe, tente pegar a ideia geral: o retorno de cada ação depende de vários fatores e cada fator tem uma “força”.
De maneira geral, o fator com o maior autovalor representa o mercado como um todo. Os fatores com menores autovalores, provavelmente são ruídos, acasos do mercado, sem relação com notícias ou eventos reais. Para traçar a linha de corte, onde é informação e onde é ruído, usamos o Teorema de Marcenko-Pastur. Esse teorema nos diz que autovetores que tenham um autovalor menor que ⋋+ são estatisticamente ruído, enquanto que os maiores são sinais:

Com m o número de ativos e n o número de dias. No nosso exemplo, ⋋+ = 1.815, temos 3 fatores significativos e o resto é ruído. Todos os autovalores que forem menores que isso, vamos trocar pela média deles, assim o ruído fica “igualmente distribuído” e tende a ser minimizado, essa técnica é conhecida como Denoising. Isso é implementado na função CleanMatrix:
def cleanMatrix(retData):
m = len(retData.T)
n = len(retData)
lambda_p = (1+np.sqrt(m/n))**2
corr = retData.corr()
eig = np.linalg.eig(corr)
eigva = eig[0]
eigve = eig[1]
cutoff = len(eigva[eigva > lambda_p])
eigva[cutoff:] = sum(eigva[cutoff:])/len(eigva[cutoff:])
corr = np.matmul(eigve, np.matmul(np.diag(eigva),eigve.T))
return pd.DataFrame(corr, index=retData.columns, columns=retData.columns)
O resultado dessa função não é uma matriz de correlação de verdade, porque os valores não ficam necessariamente entre -1 e +1 e os valores da diagonal não são 1 (a correlação da algo consigo mesmo é 1). Vamos incluir uma pequena função para corrigir isso:
# Lopez de Prado 2020
def cov2corr(cov):
# Derive the correlation matrix from a covariance matrix
std=np.sqrt(np.diag(cov))
corr=cov/np.outer(std,std)
corr[corr<-1],corr[corr>1]=-1,1 # numerical error
return corr
Vamos ver os novos heatmap e o Dendrograma:


O Dendrograma melhorou um pouco, mas ainda tem algumas coisas estranhas. Por outro lado, repare como o heatmap ficou mais homogêneo, as correlações foram puxadas para mais próximo da média geral. Na matrix suja, as correlações muito altas podem ser resultados de superestimação, enquanto as muito baixas resultado de subestimação. Em estatística, essa “puxada” em uma direção é chamada de Shrinkage. Além de deixar o heatmap mais bonito, isso tem um efeito prático importante na construção de portfólios.
Portfólio de Mínima Variância
A Teoria Moderna de Portfólios (MPT) é polêmica, cheia de defeitos e problemas práticos, mas é um bom ponto de partida para analisar coisas novas. Um dos resultados da MPT é o Portfólio de Mínima Variância (GMV), uma carteira que teoricamente tem a menor volatilidade possível, usando somente esses ativos. Para o leitor interessado, sugiro [2]. O cálculo dos pesos dos ativos e da volatilidade esperada é feita em:
## Pesos e desvio Padrão do Portfólio de Variância Mínima
def minVarPort(CorrelMatrix, standDevi):
# https://bookdown.org/compfinezbook/introcompfinr/Determining-the-Global.html
# Transforma o desvio padrão e matriz de correlação em matriz de covâriancia
covMatrix = np.matmul(np.diag(standDevi*np.sqrt(252)),np.matmul(CorrelMatrix,np.diag(standDevi*np.sqrt(252))))
newCov = 2*covMatrix.copy()
newCov.loc[-1] = np.ones(len(covMatrix))
newCov['one'] = np.append(np.ones(len(covMatrix)), 0)
b = np.append(np.zeros(len(covMatrix)), 1)
w = np.matmul(np.linalg.inv(newCov),b.T)[:-1]
std = np.sqrt(np.matmul(w.T,np.matmul(covMatrix,w)))
return w, std
Um dos motivos do GMV falhar é justamente o ruído. As carteiras obtidas pela MPT tem o inconveniente de, matematicamente, maximizarem o ruído.
Vamos comparar as carteiras obtidas utilizando a matriz de correlação original e a matriz limpa:

Na matriz limpa, os pesos que na matriz suja eram muito positivos ou muito negativos, tendem a ser puxados para o meio, enquanto que os pesos que eram próximos de zero tendem a ficar maiores (seja negativa ou positivamente). Isso cria um portfólio mais equilibrado com os pesos mais bem distribuídos, o que a primeira vista é bom.
Mas vamos para os resultados, vamos ver qual foi a volatilidade de cada portfólio nos dados de teste (que não foram usados para calcular os pesos):
- Original: 41.14% a.a.
- Limpa: 32.24% a.a.
Ponto para matriz limpa! Para efeito de comparação, a volatilidade do portfólio GMV usando a matriz de correlação dentro da amostra, impossível na prática, mas que é o melhor que pode ser alcançado, foi de:
- Dentro da Amostra: 26.39% a.a.
Chegamos muito próximo do limite teórico. Claro, utilizando ações diferentes e períodos diferentes, o resultado pode não ser tão bom, mas a expectativa é essa vista.
De Volta Aos Agrupamentos
Lembra que o Dendrograma ainda não estava muito legal? Que ainda tinha conexões meio sem sentido? Isso acontece pois havia um sinal muito forte ofuscando os outros sinais, o sinal do mercado. Fazendo uma analogia, se você está numa sala, você consegue ouvir e prestar atenção nas diferente conversas acontecendo em paralelo. Você consegue “agrupar” as diferentes conversas. Mas se uma música alta começa a tocar, fica difícil até de ouvir a conversa do seu lado. Queremos silenciar a música do mercado e ouvir a conversa dos diferentes setores.
Faremos isso utilizando a técnica de Detoning. Ela é similar ao Denoising, mas ao invés de trocarmos os autovalores menores dos ruídos pela média deles, eliminamos o autovalor e o autovetor do maior componente (o fator mercado).
def detonMatrix(retData):
m = len(retData.T)
n = len(retData)
lambda_p = (1+np.sqrt(m/n))**2
corr = retData.corr()
eig = np.linalg.eig(corr)
eigva = eig[0][1:]
eigve = eig[1].T[1:].T
corr = np.matmul(eigve, np.matmul(np.diag(eigva),eigve.T))
return pd.DataFrame(corr, index=retData.columns, columns=retData.columns)
O heatmap fica mais bagunçado:

Mas olhe esse Dendrograma, que coisa linda:

Esse dendrograma foi construído utilizando o mesmo algoritmo, está apresentando apenas de uma maneira diferente.
No começo temos as empresas relacionadas com commodities pesados (BRAP4 não é do Bradesco? Sim, mas é um grande acionista da Vale), depois frigoríficos, próximos à celulose (ambos exportadores, relacionados com fazendas e consumo) e em laranja o setor financeiro. Vale notar que esses agrupamentos por cores são feitos pelo próprio algoritmo, quando ele identifica que um conjunto é relacionado o suficiente para ser um grupo próprio. Podemos achar várias combinações que fazem muito sentido e as combinações que “não fazem sentido”, será que não fazem mesmo ou os dados estão vendo algo que nós não estamos?
Um detalhe importante é que a matriz gerada pelo Detoning não pode ser utilizada em otimização de portfólio, mas o Dendrograma gerado por ela pode ser uma poderosa ferramenta de seleção de pares para operações de Long-Short, por exemplo.
Há muito hype e mitos sobre o uso de Machine Learning em finanças, mas sabendo separar o joio do trigo, há ferramentas excelentes.
[1] DE PRADO, Marcos M. López. Machine learning for asset managers. Cambridge University Press, 2020.
[2] https://bookdown.org/compfinezbook/introcompfinr/Determining-the-Global.html
[3] https://en.wikipedia.org/wiki/Marchenko–Pastur_distribution
[4] https://en.wikipedia.org/wiki/Principal_component_analysis
