Olá pessoal,
No episódio #003 da série Code Night adicionei a possibilidade de se executar backtests nos ativos do S&P 500. Na verdade, agora é possível utilizar os principais ativos americanos em todas as ferramentas da plataforma (incluindo Long & Short por Cointegração, Retorno Histórico, VaR, etc).
A seguir um resumo do que foi coberto na live. Se você ainda não é inscrito, considere se inscrever no nosso YouTube! Falta muito pouco para atingirmos a marca de 1000 inscritos.
Fazendo o scraping do S&P 500
Antes de mais nada é necessário ter a lista dos ativos que compõe o índice americano. Ora, como a composição do índice sempre se altera, é conveniente escrever um script capaz de buscar o portfólio mais atualizado (semelhante ao que fizemos para o IBX-100 no primeiro episódio da série).
Diferentemente do IBX, no entanto, buscar os ativos do S&P 500 é bem mais fácil pois existem páginas estáticas com sua composição. Por exemplo, o Slickcharts.
O único cuidado que precisamos ter é a restrição anti-bot do Cloudflare. Felizmente é possível bypassar essa proteção com a biblioteca clouscraper.
O seguinte código retorna um dataframe já formatado com os ativos do S&P 500:
import io
import cloudscraper
import pandas as pd
scraper = cloudscraper.create_scraper()
# Web scraping to get S&P data
url = "https://www.slickcharts.com/sp500"
r = scraper.get(url)
# Convert table into df
df = pd.read_html(io.StringIO(r.text))[0][["Company", "Symbol", "Weight"]]
# Renaming for convenience
df.rename(
columns={"Company": "name", "Symbol": "symbol", "Weight": "weight"},
inplace=True,
)
df["weight"] = df["weight"].str.replace("%", "").str.strip()
df["weight"] = pd.to_numeric(df["weight"]).round(4)
Gerando um JSON com todos os ativos
Uma vez que temos o portfólio salvo no banco de dados, o próximo passo é transformar esses ativos em um JSON que possa ser consumido no frontend. Assim, quando o usuário quiser selecionar um ativo na interface, mostramos apenas os tickers suportados.
Existem diversas formas de se fazer isso. Como a lista de ativos não é tão grande (~68kB), a forma mais simples é gerar um .json
localmente e importá-lo diretamente no código. Caso o arquivo fosse maior, seria prudente fazer o upload na nuvem (num S3 da vida, por exemplo), e carregá-lo de forma assíncrona no frontend. O trade-off aqui é um menor bundle, ou seja, uma página mais leve e portanto mais rápida de carregar, contra um bundle maior mas que não necessita de uma requisição HTTP extra.
O código completo é o seguinte:
import json
import sys
from quantbrasil.crypto import crypto_tickers
from quantbrasil.models import portfolio
from quantbrasil.models.asset import get_all
from quantbrasil.utils import listed_in_the_us, symbols_with_data
# USAGE: $ python assets_json.py <PATH_TO_DIRECTORY>
# e.g. poetry run python scripts/populate/generate_assets_json.py ~/apps/quantbrasil/monorepo/apps/web/data/ # noqa: E501
args = sys.argv[1:]
path = args[0]
sp500_tickers = portfolio.get_tickers("S&P500")
symbols = symbols_with_data() + crypto_tickers + sp500_tickers + listed_in_the_us
assets_info = get_all(symbols)
assets_json = []
for asset in assets_info:
assets_json.append(
{
"symbol": asset.symbol,
"type": asset.type,
"sector": asset.sector,
}
)
file_name = "/assets.json"
with open(path + file_name, "w") as f:
json.dump(assets_json, f, ensure_ascii=False)
print("Script Successfully Executed!")
Basicamente listamos todos os ativos que queremos disponibilizar para o usuário, incluindo os que compõe o S&P 500 (esses vem direto do portfólios, enquanto os outros são hardcoded pois seus dados são salvos através do MetaTrader).
Depois de uma manipulação simples salvamos o dicionário como JSON no path passado via argumento no script. Isso permite que após a execução o JSON seja salvo diretamente no repositório do frontend.
Carregando os ativos no frontend
No frontend o trabalho é apenas carregar o JSON e manipulá-lo de forma a agrupar os ativos de acordo com o seu type. Isso permite que em cada component que desejamos disponibilizar os ativos, seja possível chamar um hook da forma:
const { grouped: assets } = useAssets({ requirePremium: true, user });
Que retornará um array contendo o label de cada grupo e todos os ativos que o compõe. Aqui a configuração requirePremium
é necessárias pois alguns ativos são disponíveis apenas aos assinantes do QuantBrasil.
O maior problema nesse caso é a performance. Isso ocorre pois sempre que o usuário digita alguma coisa, uma filtragem ocorre. No desktop esse tipo de computação é negligenciável, mas dependendo do dispositivo mobile pode haver um certo lag.
Existem várias soluções pra esse problema, nenhuma muito boa. No entanto, a mais simples (e a que implementei), é a de restringir a quantidade de caracteres ao realizar a filtragem. Porém essa restrição não pode ser grande no caso dos ativos americanos, pois existem tickers tão pequenos quanto “F” ou “A”.
Para minimizar esse problema, a estratégia se resume a:
Requerer pelo menos 1 caractere digitado para mostrar as opções. Isso impede que o usuário possa scrollar pela lista inteira – isso não é um problema pois não se espera que uma lista com quase 1000 registros seja inspecionada manualmente.
Filtrar utilizando
startsWith
em vez deincludes
. A primeira opção é bem mais eficiente e faz mais sentido nesse contexto (assim, se o usuário digitar “A”, mostramos “AAPL” mas não “BPAC11”).
const BIG_LIST_THRESHOLD = 500;
const MIN_SEARCH_LENGTH = 1;
const isBigList = totalOptions > BIG_LIST_THRESHOLD;
const filteredOptions = useMemo(() => {
if (isBigList && inputValue.length < MIN_SEARCH_LENGTH) return [];
return availableOptions
.map(group => ({
...group,
options: group.options.filter(
(option: T) =>
option.label.toLowerCase().startsWith(inputValue.toLowerCase()) ||
option.value.toLowerCase().startsWith(inputValue.toLowerCase())
),
}))
.filter(group => group.options.length > 0);
}, [availableOptions, inputValue, isBigList]);
O experiência final é bem mais rápida e nenhum lag é notado mesmo o dropdown contendo praticamente 1000 items.
A ideia para esse episódio veio da sugestão de um inscrito por email. Existem muitas formas de interagir com a gente: seja por aqui, por email, pelos comentários do YouTube ou pelo formulário de feedback do QuantBrasil, mande sua sugestão e quem sabe ela não estará em um episódio futuro do Code Night?
Nos falamos em breve,
Rafael