Tutorial por bolsai 11 mai 2026 12 min de leitura

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:

  1. Crie uma conta gratuita em usebolsai.com e copie a API key no dashboard.
  2. Instale o cliente HTTP. httpx e requests funcionam; o exemplo usa httpx pela tipagem moderna e suporte a async.
  3. Defina API_KEY e BASE uma 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}")
Ticker: ITUB4 P/L: 9.99 P/VP: 2.13 ROE: 21.32% LPA: R$ 3.81 Net Margin: 28.7% Mkt Cap: R$ 372,940,000,000

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.")
Buscou 15 de 15 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}%")
BBAS3 P/L= 4.71 P/VP=0.92 ROE= 19.8% PETR4 P/L= 5.32 P/VP=1.42 ROE= 26.6% CMIG4 P/L= 5.18 P/VP=1.01 ROE= 22.9% ITSA4 P/L=10.31 P/VP=1.18 ROE= 16.4%

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}")
PETR4 DY=12.30% ROE= 26.6% P/L= 5.32 TAEE11 DY=10.85% ROE= 21.6% P/L= 9.85 BBAS3 DY= 9.74% ROE= 19.8% P/L= 4.71 CMIG4 DY= 9.42% ROE= 22.9% P/L= 5.18 ITSA4 DY= 8.91% ROE= 16.4% P/L=10.31 VIVT3 DY= 8.55% ROE= 13.1% P/L= 8.92

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.

Criar conta gratuita

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}%")
BBAS3 EY=21.23% ROE= 19.8% PETR4 EY=18.80% ROE= 26.6% CMIG4 EY=19.31% ROE= 22.9% TAEE11 EY=10.15% ROE= 21.6% ITUB4 EY=10.01% ROE= 21.3% ITSA4 EY= 9.70% ROE= 16.4% VIVT3 EY=11.21% ROE= 13.1% BBSE3 EY= 8.93% ROE= 35.4%

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))
ticker pl pvp roe net_margin net_debt_ebitda dividend_yield market_cap PETR4 5.32 1.42 26.60 22.23 1.18 12.30 6.31e+11 TAEE11 9.85 1.46 21.60 28.10 2.31 10.85 3.42e+10 BBAS3 4.71 0.92 19.80 16.40 NaN 9.74 1.20e+11 CMIG4 5.18 1.01 22.90 14.20 1.07 9.42 3.18e+10 ITSA4 10.31 1.18 16.40 9.80 0.62 8.91 9.65e+10 VIVT3 8.92 1.07 13.10 11.50 0.88 8.55 7.81e+10

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

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.