Como construir um screener de ações em Python usando uma API de mercado
Construir um screener de ações em Python deixou de exigir scraping frágil ou planilhas manuais. Com uma API REST que entrega indicadores fundamentalistas prontos, o caminho do "como criar screener de ações no Brasil" cabe em um arquivo de menos de cem linhas. Este tutorial parte de uma única requisição HTTP, evolui até listas filtradas com regras estilo Graham, Bazin e Magic Formula, e termina com exportação para CSV e pandas. Todo o código é copiável e funciona no plano gratuito.
O que é um screener de ações e por que escrever o seu
Um screener de ações é uma rotina que filtra o universo da B3 (cerca de 264 papéis líquidos) por critérios objetivos como P/L abaixo de 12, ROE acima de 15% ou dividend yield acima de 6%. Em vez de analisar empresa por empresa, o investidor descreve em código o perfil que procura e recebe apenas os tickers que atendem todas as regras. Em Python, com uma API de mercado, isso se traduz em uma chamada por ticker, uma lista de comparações e uma ordenação.
Plataformas como Fundamentus e Status Invest oferecem interface visual, mas não permitem automação confiável. A diferença em rodar um screener próprio em Python está no controle: salvar o histórico das execuções, comparar resultados ao longo do tempo, alimentar um backtest, integrar com Telegram, Discord ou Slack para alertas. Esta é a habilidade prática que o tutorial entrega.
Setup: API key e dependências
Antes do código, três passos rápidos:
- Crie uma conta gratuita em usebolsai.com e copie a API key no dashboard.
- Instale o cliente HTTP.
httpxerequestsfuncionam; o exemplo usahttpxpela tipagem moderna e suporte a async. - Defina
API_KEYeBASEuma vez, reaproveite em todas as chamadas.
pip install httpx pandas
Todas as chamadas autenticam pelo header X-API-Key. A base é
https://api.usebolsai.com/api/v1. O plano gratuito libera 200 requisições por dia, o que dá folga para um screener
diário sobre 30 a 60 tickers selecionados.
Passo 1: fundamentos de uma única ação
O endpoint /fundamentals/{ticker} retorna todos os indicadores fundamentalistas calculados em TTM (últimos 12 meses)
para um ticker. É a peça atômica do screener. Antes de iterar sobre uma lista, vale entender a resposta para um único papel.
A definição de cada métrica está em
Análise fundamentalista para iniciantes,
que cobre P/L, P/VP, ROE e EV/EBITDA com exemplos.
import httpx
API_KEY = "sua_chave_aqui"
BASE = "https://api.usebolsai.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}
r = httpx.get(f"{BASE}/fundamentals/ITUB4", headers=HEADERS)
r.raise_for_status()
data = r.json()
print(f"Ticker: {data['ticker']}")
print(f"P/L: {data['pl']}")
print(f"P/VP: {data['pvp']}")
print(f"ROE: {data['roe']}%")
print(f"LPA: R$ {data['lpa']}")
print(f"Net Margin: {data['net_margin']}%")
print(f"Mkt Cap: R$ {data['market_cap']:,.0f}")
O JSON cobre 27 campos: pl, pvp, ev_ebitda, ev_ebit, p_sr,
roe, roa, roic, lpa, vpa, net_margin,
gross_margin, ebit_margin, ebitda_margin, debt_equity,
net_debt_ebitda, current_ratio, cagr_revenue_5y, cagr_earnings_5y,
market_cap e contas brutas como net_income, equity e net_revenue.
Um detalhe importante: o dividend yield não está aqui. Como o histórico de proventos vem de uma
fonte distinta (CVM provento + B3 calendário), o campo dividend_yield_ttm mora em /dividends/{ticker}.
O passo 4 mostra como combinar os dois.
Passo 2: iterar sobre uma lista de tickers
Um screener começa a fazer sentido quando varre dezenas de papéis. A forma mais simples é um loop sobre uma lista curada. Para
amenizar o custo de rede, o httpx.Client() reutiliza a conexão TCP entre requisições, o que reduz o tempo total em
cerca de 60% comparado a chamadas isoladas.
import httpx
TICKERS = [
"PETR4", "VALE3", "ITUB4", "BBAS3", "BBDC4",
"WEGE3", "ABEV3", "MGLU3", "VIVT3", "CMIG4",
"TAEE11", "BBSE3", "ITSA4", "SUZB3", "RENT3",
]
def fetch_fundamentals(client, ticker):
"""Busca fundamentos. Devolve dict ou None se falhar."""
try:
r = client.get(f"{BASE}/fundamentals/{ticker}")
if r.status_code == 200:
return r.json()
except httpx.HTTPError:
pass
return None
with httpx.Client(headers=HEADERS, timeout=10.0) as client:
rows = [fetch_fundamentals(client, t) for t in TICKERS]
rows = [r for r in rows if r]
print(f"Buscou {len(rows)} de {len(TICKERS)} tickers.")
O try/except e a checagem de status_code são deliberados. Em produção, papéis em situação especial
(recuperação judicial, BDR, suspensos) podem devolver 404 ou 422, e descartar silenciosamente evita derrubar todo o pipeline.
Para 200 a 500 tickers, considere agendar uma vez por dia ou usar o
endpoint /screener pré-computado
discutido no passo final.
Passo 3: aplicar filtros e ordenar
Com a lista de dicts em mãos, filtrar é só uma list comprehension. O exemplo abaixo aplica três regras combinadas: P/L entre 0 e 12 (positivo para excluir prejuízo, baixo para excluir caro), ROE acima de 15% (sinal de rentabilidade real) e P/VP abaixo de 2 (limite contra exagero patrimonial). Em seguida ordena pelo P/L crescente.
def passa_no_filtro(r):
if r["pl"] is None or r["roe"] is None:
return False
return (
0 < r["pl"] < 12
and r["roe"] > 15
and r["pvp"] < 2
)
candidatos = [r for r in rows if passa_no_filtro(r)]
candidatos.sort(key=lambda r: r["pl"])
for r in candidatos:
print(f"{r['ticker']:6} P/L={r['pl']:>5.2f} "
f"P/VP={r['pvp']:>4.2f} ROE={r['roe']:>5.1f}%")
O tratamento de None não é cosmético. Empresas com prejuízo nos últimos 12 meses retornam pl=None da API,
já que dividir preço por lucro negativo perde sentido econômico. Ignorar essa checagem produz TypeError quando o
comparador encontra None. A regra 0 < pl < 12 resolve o caso de prejuízo (filtra ações com lucro
negativo via API que vem com pl negativo em alguns casos), e a verificação explícita garante segurança.
Passo 4: incluir dividend yield combinando endpoints
Para um screener focado em renda, o filtro mais útil é dividend yield mínimo, e o DY vive em /dividends/{ticker} sob
o campo dividend_yield_ttm. A solução é uma segunda chamada por ticker. O custo dobra (uma requisição vira duas), mas
no plano gratuito 200 requisições/dia ainda comportam 100 papéis. Detalhes sobre o cálculo do indicador estão em
Dividend Yield via API.
def enrich_with_dy(client, fund_row):
ticker = fund_row["ticker"]
r = client.get(f"{BASE}/dividends/{ticker}")
if r.status_code == 200:
fund_row["dividend_yield"] = r.json().get("dividend_yield_ttm")
else:
fund_row["dividend_yield"] = None
return fund_row
with httpx.Client(headers=HEADERS, timeout=10.0) as client:
enriched = [enrich_with_dy(client, r) for r in rows]
renda = [
r for r in enriched
if r.get("dividend_yield") and r["dividend_yield"] > 6
and r.get("roe") and r["roe"] > 12
]
renda.sort(key=lambda r: r["dividend_yield"], reverse=True)
for r in renda:
print(f"{r['ticker']:6} DY={r['dividend_yield']:>5.2f}% "
f"ROE={r['roe']:>5.1f}% P/L={r['pl']:>5.2f}")
A combinação ROE acima de 12% com dividend yield acima de 6% elimina o cenário comum em que uma queda forte de preço infla o yield artificialmente sem geração real de lucro. O artigo Como montar carteira de dividendos com Python aprofunda o uso desses filtros num portfólio diversificado.
Pegue sua API key e rode os exemplos agora
Plano gratuito com 200 requisições por dia. Sem cartão de crédito. Login com Google.
Passo 5: três estratégias clássicas como filtros
Strategies famosos viraram screeners por uma razão prática: cada um descreve um perfil de ação em meia dúzia de regras objetivas. Vale codificar as três mais conhecidas como funções reutilizáveis.
Graham (valor profundo)
Benjamin Graham, em The Intelligent Investor, sugeriu a régua P/L abaixo de 15, P/VP abaixo de 1,5 e o produto P/L × P/VP menor que 22,5. A versão "defensiva" também exige liquidez razoável, aproximada por market cap mínimo. O artigo dedicado Fórmula de Graham para ações brasileiras cobre a adaptação completa.
def graham(r):
if not r.get("pl") or not r.get("pvp"):
return False
return (
0 < r["pl"] < 15
and r["pvp"] < 1.5
and r["pl"] * r["pvp"] < 22.5
and r["market_cap"] > 3e9
)
Bazin (dividendos sustentáveis)
Decio Bazin, em Faça Fortuna com Ações, Antes que Seja Tarde, popularizou no Brasil a regra do "preço justo" via DY mínimo de 6% sustentado por anos. Em termos práticos: DY acima de 6%, payout (proporção de lucro distribuído) razoável e dívida controlada. Como payout depende de cálculos derivados, a aproximação aqui usa DY mínimo combinado com ROE positivo e dívida líquida sobre EBITDA contida.
def bazin(r):
dy = r.get("dividend_yield")
nde = r.get("net_debt_ebitda")
if not dy or not r.get("roe"):
return False
return (
dy > 6
and r["roe"] > 10
and (nde is None or nde < 2.5)
)
O nde is None or nde < 2.5 é importante: bancos retornam net_debt_ebitda como None porque
o indicador não se aplica ao setor (dívida é matéria-prima). Excluí-los pelo nulo derrubaria o screener de dividendos em qualquer
rodada, e bancos são justamente pagadores tradicionais. A mais sobre a métrica está em
Dívida Líquida / EBITDA via API.
Magic Formula (qualidade e preço combinados)
Joel Greenblatt, em The Little Book That Beats the Market, propôs ranquear cada ação por earnings yield (proxy: 1/P/L) e retorno sobre capital (proxy: ROE ou ROIC), depois somar as duas posições no ranking. A carteira dos 20 a 30 primeiros costuma bater o índice em janelas longas.
def magic_formula(rows):
elegiveis = [r for r in rows
if r.get("pl") and r["pl"] > 0
and r.get("roe")]
by_ey = {r["ticker"]: i for i, r in
enumerate(sorted(elegiveis, key=lambda x: x["pl"]))}
by_roe = {r["ticker"]: i for i, r in
enumerate(sorted(elegiveis, key=lambda x: -x["roe"]))}
return sorted(
elegiveis,
key=lambda r: by_ey[r["ticker"]] + by_roe[r["ticker"]]
)
ranking = magic_formula(enriched)[:8]
for r in ranking:
ey = 100 / r["pl"]
print(f"{r['ticker']:6} EY={ey:>5.2f}% ROE={r['roe']:>5.1f}%")
O resultado típico em mercados emergentes é uma lista dominada por bancos, utilities e seguradoras. Greenblatt recomenda construir a carteira com 20 a 30 nomes e revisar a cada 12 meses, padrão difícil de manter sem automação. Para uma comparação aprofundada entre Graham e a Magic Formula, vale o artigo da fórmula de Graham.
Passo 6: organizar em DataFrame e exportar para CSV
A maioria das análises termina em planilha. Pandas resolve a conversão JSON → DataFrame → CSV em três linhas. O arquivo abre direto no Excel, no Google Sheets via importação manual ou no conector via Apps Script.
import pandas as pd
from datetime import date
df = pd.DataFrame(enriched)
cols = ["ticker", "pl", "pvp", "roe",
"net_margin", "net_debt_ebitda",
"dividend_yield", "market_cap"]
df = df[cols]
# Aplica filtro Bazin já no DataFrame
mask = (
(df["dividend_yield"] > 6)
& (df["roe"] > 10)
& ((df["net_debt_ebitda"] < 2.5) | df["net_debt_ebitda"].isna())
)
resultado = df.loc[mask].sort_values("dividend_yield", ascending=False)
resultado.to_csv(f"screener_bazin_{date.today()}.csv", index=False)
print(resultado.to_string(index=False))
O arquivo CSV gerado vai para a pasta de trabalho com o nome screener_bazin_2026-05-11.csv. Salvar com a data no nome
cria automaticamente um histórico das execuções, útil para identificar quando uma ação entra ou sai do filtro. O notebook do
backtest em Python
mostra como reaproveitar esses snapshots para medir performance ex-post.
Atalho: o endpoint /screener do plano Pro
Tudo o que o tutorial até aqui montou manualmente também pode ser feito em uma chamada usando o endpoint /screener,
disponível no plano Pro (R$29/mês). Ele varre o universo inteiro de 264 ações, aceita filtros no formato {metrica}_gt /
{metrica}_lt e devolve até 500 linhas com todos os indicadores já populados. Como o snapshot é pré-computado uma vez
por dia, vale 1 requisição na cota independentemente do filtro.
params = {
"pl_gt": 0,
"pl_lt": 12,
"roe_gt": 15,
"dividend_yield_gt": 6,
"net_debt_ebitda_lt": 2.5,
"sort": "dividend_yield",
"order": "desc",
"limit": 20,
}
r = httpx.get(f"{BASE}/screener", params=params, headers=HEADERS)
for row in r.json()["data"]:
print(f"{row['ticker']:6} DY={row['dividend_yield']:>5.2f}% "
f"ROE={row['roe']:>5.1f}% P/L={row['pl']:>5.2f}")
A escolha entre rodar um screener próprio com /fundamentals ou usar /screener direto depende do estágio.
Para prototipar uma estratégia ou trabalhar com uma lista curada de 30 a 60 ações, o plano gratuito basta. Para varrer o universo
inteiro diariamente, comparar dezenas de estratégias ou alimentar dashboards em tempo quase real, o Pro elimina o atrito. A
referência completa do /screener
detalha todos os 25 parâmetros disponíveis.
| Abordagem | Chamadas/dia | Custo | Melhor para |
|---|---|---|---|
| Lista curada + /fundamentals | ~50 | Gratuito (200/dia) | Aprender, prototipar, screening tático |
| /fundamentals + /dividends | ~100 | Gratuito (200/dia) | Screening de dividendos sem upgrade |
| /screener pré-computado | 1 por execução | Pro (R$29/mês) | Universo completo, dashboards diários |
Limitações e armadilhas comuns
Um screener é uma ferramenta de pré-seleção, não de decisão. Três armadilhas frequentes:
Comparar setores incomparáveis. P/L de banco e P/L de tech crescimento contam histórias diferentes,
e tetos universais escondem isso. Bancos têm balanço estruturalmente distinto: net_debt_ebitda não se aplica porque a
dívida é insumo, e o EBITDA tradicional perde sentido econômico. O parâmetro sector ajuda a circunscrever o filtro
ao grupo relevante. O artigo
Análise fundamentalista para iniciantes
discute as quatro dimensões com exemplos por setor.
Confundir liquidez com tamanho. Empresas com market cap acima de R$ 1 bilhão podem ter ADTV (volume médio diário) baixíssimo, o que torna a entrada/saída cara. O screener não substitui a checagem do volume real no endpoint de histórico de preços, que entrega volume diário ajustado.
Tratar o resultado como recomendação. Passar no filtro significa "merece ser estudado", não "deve ser comprado". A análise qualitativa (modelo de negócios, governança, dinâmica do setor, riscos regulatórios) precisa ser feita separadamente sobre os tickers que sobraram. Um bom resultado quantitativo não compensa uma tese qualitativa frágil.
Perguntas frequentes
Preciso de um plano pago para construir um screener?
Não. O plano gratuito da bolsai dá 200 requisições por dia no endpoint /fundamentals/{ticker}, suficiente para varrer
uma lista curada de 30 a 50 tickers e aplicar filtros no cliente. O endpoint /screener pré-computado, que devolve as
264 ações em uma chamada, é exclusivo do plano Pro (R$29/mês) e funciona como atalho para quem não quer iterar.
Por que dividend yield não aparece em /fundamentals/{ticker}?
O endpoint /fundamentals/{ticker} foca em indicadores derivados de DFP e ITR (DRE, balanço, DFC). O dividend yield
depende do histórico de proventos pagos nos últimos 12 meses, que tem fonte separada (CVM provento + B3 calendário) e é entregue
pelo endpoint /dividends/{ticker} sob o campo dividend_yield_ttm. O endpoint /screener
consolida ambos em um único snapshot.
Quantos tickers posso filtrar com o plano gratuito?
Cada chamada a /fundamentals/{ticker} consome uma requisição. Com 200 requisições/dia gratuitas, dá para varrer cerca
de 100 tickers se você consultar apenas fundamentos, ou 50 se combinar com /dividends/{ticker} para DY. O caminho
prático é manter uma lista curada de 30 a 60 candidatos relevantes e atualizar o snapshot uma vez por dia.
Como salvar o screener para rodar todo dia?
Como o screener é apenas um script Python que faz chamadas HTTP, basta agendá-lo. As três opções comuns são cron local, GitHub
Actions com schedule, ou um worker em serviço como Railway, Fly.io ou Render. O snapshot dos fundamentos é atualizado
uma vez por dia após o fechamento do pregão, então rodar antes das 20h BRT devolve dados do dia anterior.
Leia também
- Análise fundamentalista para iniciantes: explicação prática de P/L, P/VP, ROE e EV/EBITDA com exemplos.
- Fórmula de Graham para ações brasileiras: adaptação completa da régua clássica ao mercado local.
- Dividend Yield via API: cálculo do indicador, fontes de dados e armadilhas.
- Backtesting de ações em Python: como medir performance ex-post de estratégias.
- Referência do endpoint /screener: todos os parâmetros e exemplos prontos.
Disclaimer: Este conteúdo é educacional e não constitui recomendação de investimento. Os tickers citados servem para ilustrar o funcionamento técnico dos endpoints com base em snapshot público. Decisões de alocação envolvem risco e devem considerar perfil pessoal, horizonte e contexto macroeconômico. Os exemplos numéricos refletem o snapshot de referência de abril de 2026 e mudam ao longo do tempo.
por bolsai · 11 de maio de 2026 · 12 min de leitura
Comece agora
A barreira para construir screener de ações em Python no Brasil deixou de ser técnica: hoje é apenas escrever a tese de investimento
em código. O plano gratuito dá 200 requisições por dia, o suficiente para rodar os exemplos deste tutorial sobre uma lista curada de
40 a 60 papéis. Quando o universo expande para as 264 ações ou para múltiplas estratégias paralelas, o plano Pro com endpoint
/screener pré-computado economiza tempo de desenvolvimento.
Fontes oficiais utilizadas pela bolsai: B3, CVM e Banco Central. Conheça também a integração via MCP para usar a API com Claude e outros agentes de IA.