Prevendo o Mercado Financeiro com Redes Neurais Artificiais — Parte II: Investindo no Mercado Financeiro
Chegou a hora de avaliar com um cenário real a RNA construída no artigo anterior e investir no mercado financeiro
Tendo como base o conteúdo introduzido aqui sobre Redes Neurais Artificiais (RNA) do tipo Multilayer Perceptron (MLP) e o conteúdo apresentado aqui em que começamos a aplicação de uma MLP para previsão do mercado financeiro, neste artigo iremos incrementar o nosso código inicial e desenvolver a simulação de uma estratégia de investimento. Assim, conseguiremos mostrar como uma RNA pode ser utilizada para investir no mercado financeiro — e fazer um dinheiro extra, principalmente em épocas de crise, como essa do COVID-19.
Observação
Nessa atual versão de minha implementação, optei por revisar alguns detalhes que desenvolvi no post anterior. Os pontos que são similares, não explicarei a fundo, me atendo mais às mudanças e à estratégia de investimento ao final. Se você ainda não leu, recomendo a leitura aqui para ter o background geral antes de se aprofundar neste artigo.
Carregando, Explorando e Visualizando
Para esse experimento, vamos utilizar dados de cotações diárias da Bovespa, de 2010 a 2020. Essa grande quantidade de dados nos permite capturar diferentes períodos de nossa economia, sejam de crise, sejam de otimismo econômico. Com isso, esperamos que nossa Rede Neural Artificial será treinada para lidar também com períodos complexos como o que estamos passando agora, melhorando a sua taxa de acertos do movimento do mercado financeiro.
Vamos, então, carregar as bibliotecas necessárias, ler o arquivo CSV em um DataFrame e descrevê-lo, para que possamos ter uma visão geral do que estamos lidando. Além disso, determinamos que o índice de nosso DataFrame seja a data, para que possamos manipular mais facilmente os períodos de tempo da cotação do índice.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#Carregando os dados
data = pd.read_csv(“./Data/Stocks/^BVSP.csv”, usecols= [‘Date’,’Open’, ‘High’, ‘Low’, ‘Close’, ‘Volume’])
data[‘Date’] = pd.to_datetime(data[‘Date’])
data.set_index(‘Date’, inplace=True)
data.describe()
Com isso, podemos obter as seguintes informações: os maiores (max) e menores(min) valores; a média (mean) e desvio padrão (std) desses valores; bem como também obter a informação da distribuição dos dados sobre os seguintes quartis: 25%, 50% e 75%.
Vamos continuar nossa exploração dos dados:
data.info()
O resultado nos mostra que possuímos 2480 entradas de dados, mas que 2473 deles por coluna são não-nulos. Isso significa que temos, portanto, 7 entradas por coluna que são nulas. Isso pode nos atrapalhar em nosso desenvolvimento. Assim, iremos eliminar todos os dados nulos, que coincidentemente são comuns a todas as colunas.
data = data.dropna()
Agora, chegou a hora de explorar um pouco mais os dados, exibindo os candlesticks para podermos entender a evolução do preço das ações do Bovespa ao longo do tempo. Para não poluir a imagem, irei exibir apenas de janeiro de 2019. Esse tipo de gráfico é facilmente exibido por meio da biblioteca mplfinance.
#Plotting...
import mplfinance as mpf
graph = data.copy()
mpf.plot(data[pd.to_datetime('01-01-2019'):pd.to_datetime('02-01-2019')], type='candle', figscale=1.0, volume=False, style='charles', ylabel='Ibovespa (R$)')
Extraindo Características
Agora que já exploramos os dados, a segunda etapa deste experimento consiste em criar características para trabalharmos em nosso modelo.
O primeiro passo é identificar o que iremos prever. Nesse caso, queremos prever o retorno do preço de fechamento do dia seguinte do mercado financeiro. Ou seja, queremos a partir dos dados do dia de hoje prever quanto irei ganhar se investir no mercado financeiro amanhã.
Todo o processamento será feito sobre o DataFrame que contém os dados de cotações do Bovespa. Para isso, se deslocarmos (shift) o preço de fechamento em uma posição para trás, já obtemos o preço de fechamento do dia seguinte. Em seguida, se subtrairmos o preço de fechamento de hoje do preço de fechamento do dia seguinte, seremos capazes de obter o retorno de investimento do dia seguinte. Confuso? Talvez o código ajude. :)
#Extracting Features
data['Close_Tomorrow'] = data['Close'].shift(-1)
data['Return'] = data['Close_Tomorrow'] - data['Close']
data
Como pode-se observar, agora temos em nosso DataFrame não somente os dados do dia do Bovespa, mas também os dados do próximo dia: Close_Tomorrow e Return. Isso nos dará uma grande facilidade em termos de implementação do modelo, mais à frente.
Vale destacar que a última linha possui alguns valores nulos para o valor de fechamento e retorno de investimento do dia seguinte (porque? :).
Normalização dos Dados de Entrada
Prosseguindo, os dados precisam ser normalizados antes de serem entradas para nossa RNA do tipo Multilayer Perceptron (conforme expliquei em mais detalhes no artigo da Parte I).
Nesse caso, percorremos cada coluna do nosso DataFrame, normalizando-a e criando uma nova coluna no próprio DataFrame mantendo o nome adicionado da extensão “_Norm”. Dessa forma, vamos adicionando em nosso DataFrame os respectivos valores normalizados, evitando perder informações e facilitando quando precisarmos recuperar os valores originais.
Já aproveitando, vamos retirar também os dados nulos.
Todos esses pontos são realizados como apresentado a seguir.
#Normalizing
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
for c in data.columns:
data[c+'_Norm'] = scaler.fit_transform(data[c].to_numpy().reshape(-1, 1))
data = data.dropna()
Divisão dos Dados de Treinamento
Agora, um momento importante: vamos dividir nossos dados para treinamento, validação e teste da nossa RNA.
Os dados de treinamento são aqueles que fazem o nosso modelo “compreender” a distribuição dos dados que queremos prever. Os dados de validação são utilizados para ajustar esse modelo durante o treinamento, enquanto que os dados de teste nunca devem ser vistos pelo modelo, sendo utilizados para verificar se o modelo é capaz de “generalizar” o conhecimento adquirido para dados novos.
Existem diferentes proporções indicadas para essa divisão. Aqui, nossos dados possuem dependência temporal (alguém aí falou LSTM?) e essa dependência deve ser mantida no treinamento da rede. Nesse experimento, optei por separar os primeiros 5 anos para treinamento, 2 para validação e o restante para teste.
Vejam como todos os dados de treinamento da rede (normalizados) são retirados do DataFrame criado e incrementado com novas colunas.
#Splitting the data
train = data[pd.to_datetime('01-01-2010'):pd.to_datetime('01-01-2015')]
train_x = data[['Open_Norm','High_Norm','Low_Norm','Close_Norm','Volume_Norm']].to_numpy()
train_y = data[['Return_Norm']].to_numpy()val = data[pd.to_datetime('01-01-2015'):pd.to_datetime('01-01-2017')]
val_x = val[['Open_Norm','High_Norm','Low_Norm','Close_Norm','Volume_Norm']].to_numpy()
val_y = val[['Return_Norm']].to_numpy()test = data[pd.to_datetime('01-01-2017'):pd.to_datetime('01-01-2020')]
test_x = test[['Open_Norm','High_Norm','Low_Norm','Close_Norm','Volume_Norm']]
test_y = test[['Return_Norm', 'Return']]
Definindo e treinando a Rede Neural Artificial (MLP)
Agora, chegou a hora de treinar nossa RNA. Para isso, defini uma rede com 4 camadas, sendo as 3 primeiras com 30 neurônios cada e função de ativação ReLU. O treinamento foi realizado por 1000 épocas.
As entradas da rede são os valores de abertura, fechamento, alta e baixa da ação do dia atual, todos normalizados na etapa anterior. A saída da rede é o retorno da ação do dia de fechamento seguinte, também já normalizado. Note que temos também dados de validação fornecidos à rede.
#Model
from keras import models
from keras import layers
from keras.layers import Dense
model = models.Sequential()
model.add(layers.Dense(30, input_dim = train_x.shape[1], activation = "relu"))
model.add(layers.Dense(30, activation ="relu"))
model.add(layers.Dense(30, activation ="relu"))
model.add(layers.Dense(1))
model.summary()model.compile(
optimizer = 'adam',
loss = 'mean_squared_error', #mean_squared_error
)
results = model.fit(
train_x, train_y,
epochs= 1000,
batch_size = 128,
validation_data = (val_x, val_y),
verbose = 0
)plt.plot(results.history["loss"], label="loss")
plt.plot(results.history["val_loss"], label="val_loss")
plt.legend()
plt.show()
Acima, temos um resumo da RNA criada, além de um gráfico dos resultados da função de erro tanto do conjunto de teste quanto do conjunto de validação. Note como eles vão diminuindo ao longo do tempo e que a sua tendência é se aproximar à medida em que aumentamos o número de épocas treinadas.
Investindo… Finalmente!
Até aqui temos uma versão atualizada e revisada do meu post anterior. Essa atualização foi extremamente necessária para chegarmos até aqui.
Vamos agora colocar tudo isso em prática e simular uma situação de investimento no período dos dados testes (2017 a 2020). Será que é possível ganhar dinheiro? Veremos…
A primeira coisa a fazermos é pôr o nosso modelo pra fazer previsões e desnormalizar o resultado, fazendo-o voltar à nossa escala original, ou seja, o valor de cada ação.
O código é apresentado a seguir:
pred = model.predict(test_x)
pred = scaler.inverse_transform(pred)
Se realizarmos a impressão desses valores, teremos um vetor de valor de ações previstos. Mas como estou trabalhando bastante com DataFrame neste artigo, vou continuar nessa linha e adicionar a coluna de previsões ao meu DataFrame de testes, para que possamos comparar o valor real com o valor previsto.
pred = model.predict(test_x)
pred = scaler.inverse_transform(pred)
test_y['Return_Predicted'] = pred
test_y
Vejamos…
Nesse Dataframe nós temos o valor do retorno normalizado. Esse é o valor esperado pela nossa predição. Temos também o valor correspondente na escala original. Por fim, adicionamos o valor do retorno previsto pela RNA. É possível verificar que existe uma certa discrepância nesses valores. Por exemplo, o dia 05–01–2017 obteve uma queda de -406, ao passo em que o valor previsto foi -462. Isso não é tão crítico se considerarmos que iremos trabalhar com o “sobe e cai” do mercado. Ou seja, não nos interessa o quanto subiu, mas se subiu. Ou se caiu. E investiremos seguindo essa idéia.
A primeira coisa a fazer nesse sentido é analisar o sentido do movimento do mercado previsto e também o sentido do mercado na realidade. Isso é feito com o código a seguir, em que percorro todos os valores previstos e reais verificando se são maiores (marcados com Up) ou menores que 0 (marcados com Down).
test_y['Movement_Predicted'] = [ 'Up' if p > 0 else 'Down' for p in pred]
test_y['Movement_Real'] = [ 'Up' if m > 0 else 'Down' for m in test_y['Return']]
test_y
Agora conseguimos comparar ambos movimentos do mercado financeiro: o que somos capazes de prever com nosso modelo e o movimento que ocorreu na realidade. Veja, por exemplo, a primeira linha: o movimento real foi para cima e conseguimos prever isso! No entanto, também erramos, como na linha de 2019–12–19, em que o mercado foi para baixo e previmos para cima. Apesar disso, vamos mostrar que é ainda possível obter lucro com essa abordagem!
Estratégia de Investimento
A nossa estratégia de investimento, que pode ser automatizada futuramente em um robô de investimentos, será bem direta: se previmos que o dia seguinte irá subir, nós investiremos. Caso contrário, não haverá investimento.
Vamos fazer tudo isso em nosso DataFrame de testes. A primeira etapa percorre todas as linhas do nosso DataFrame de testes, marcando com um acerto (Hit) todas as correspondências em que acertamos na previsão quando comparadas com o mundo real. Essa coluna é boa para medirmos o quanto acertamos em nossa previsão.
A segunda etapa, cria a coluna “Investiment_Return” e adiciona a ela todos os valores de retorno que correspondem com o movimento Up, simulando que nesses períodos nós colocamos o nosso dinheiro em ação e obtemos o resultado do retorno do dia. Essa coluna irá contabilizar o sucesso do nosso investimento.
test_y['Hit'] = [ 1 if m[1]['Movement_Real'] == m[1]['Movement_Predicted'] else 0 for m in test_y.iterrows()]test_y['Investiment_Return'] = [m[1]['Return'] if m[1]['Movement_Predicted'] == 'Up' else 0 for m in test_y.iterrows()]
Em seguida, vamos exibir os valores: média de acertos (Acc) de sobe e desce do mercado no período e o retorno total de nossa estratégia.
print('Acc: ',test_y['Hit'].mean())
print('Strategy Return: R$',test_y['Investiment_Return'].sum())
Conseguimos impressionantes 60% de acertos no movimento do mercado financeiro no período de 2017 a 2020! Isso é mais do que o suficiente para obter lucro.
A prova? Nosso experimento retornou ganhos de R$ 93.574,00 nesse período!
Mas é interessante que tenhamos um baseline para comparar. O que é isso? Um baseline nesse caso será uma estratégia de investimento à qual possamos comparar o nosso resultado para saber se ele é válido. Caso contrário, poderíamos apenas nos aproveitar de um período de alta do mercado e nosso resultado estaria enviesado.
Nosso baseline será a estratégia Buy&Hold, que significa apenas comprar e segurar a ação durante o período todo, analisando a sua valorização. Ele é a soma de todos os retornos nesse período, como mostrado a seguir.
print(' Buy & Hold: R$',test_y['Return'].sum())
>>> Buy & Hold: R$ 56375.0
Maravilha! Conseguimos superar o nosso baseline em 65%!
Ainda não acabamos! Obviamente, precisaríamos de um investimento inicial para que possamos fazer esse dinheiro todo, que é basicamente o primeiro valor de ação no período considerado:
print('\nInvestiment: R$\n',data[pd.to_datetime('01-01-2017'):].head(1)['Open'])
>>> Investiment: R$
>>> Date
>>> 2017-01-02 60227.0
>>> Name: Open, dtype: float64
Ou seja, começamos investindo com R$ 60.227,00 e conseguimos obter um retorno de até R$ 93.574,00, aumentando em 155% o nosso patrimônio!
Para finalizar, vamos apenas plotar o gráfico de candlesticks no período, para termos uma visão geral do período em que acabamos de prever.
#Plotting...
import mplfinance as mpf
graph = data.copy()
mpf.plot(data[pd.to_datetime('01-01-2017'):pd.to_datetime('01-01-2020')], type='candle', figscale=1.5, volume=False, style='charles', ylabel='Ibovespa (R$)')
Como pode-se observar, entre sobes e desces, o período de 2017 até o começo de 2020 obteve valorização em geral. Esse período compreende o pós-impeachment, eleições presidenciais e o primeiro ano do novo presidente. Analisamos, portanto, uma grande variedade de cenários e, particularmente, estou surpreso com o resultado que poderíamos ter obtido caso fizéssemos investimentos baseando simplesmente em uma Rede Neural Artificial do tipo MLP.
Um interessante trabalho futuro pode ser aplicar essa mesma rede para tentar prever o mercado em período de COVID-19. Um desafio para um próximo artigo, quem sabe? ;)
Aproveito para deixar o link para o código completo em meu GitHub aqui.
Abraços, lave as mãos e bons investimentos!