Cansado de analisar ação por ação manualmente? Aprenda a criar seu próprio filtro de ações (stock screener) em Python para encontrar oportunidades de investimento automaticamente.
O Que é um Stock Screener?
Um stock screener (ou filtro de ações) é uma ferramenta que analisa automaticamente centenas de ações e retorna apenas aquelas que atendem aos seus critérios de investimento.
Por que criar seu próprio screener?
| Problema | Solução com Screener Próprio |
|---|---|
| Analisar 400+ ações manualmente | Filtrar em segundos |
| Screeners pagos caros | Gratuito com brapi |
| Critérios limitados | Você define as regras |
| Dados desatualizados | Dados em tempo real |
| Sem automação | Roda automaticamente |
O Que Vamos Construir
Ao final deste tutorial, você terá um screener que:
- Busca todas as ações da B3
- Obtém dados fundamentalistas de cada uma
- Filtra por múltiplos critérios (P/L, ROE, Dividend Yield, etc.)
- Exporta resultados para CSV/Excel
- Pode rodar automaticamente todo dia
📊 RESULTADO DO SCREENER - 17/01/2026
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Critérios: P/L < 10 | ROE > 15% | DY > 5% | Dívida/PL < 1
✅ BBAS3 | P/L: 4.2 | ROE: 18.5% | DY: 8.2% | Setor: Bancos
✅ TAEE11 | P/L: 7.8 | ROE: 22.1% | DY: 9.5% | Setor: Energia
✅ CMIN3 | P/L: 5.1 | ROE: 28.3% | DY: 12.1% | Setor: Mineração
✅ BBSE3 | P/L: 8.9 | ROE: 19.7% | DY: 7.8% | Setor: Seguros
Total: 4 ações encontradas de 387 analisadas
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Pré-requisitos
1. Python 3.8+
python --version
# Python 3.11.02. Bibliotecas Necessárias
pip install requests pandas3. Token da brapi (Gratuito)
- Acesse brapi.dev
- Crie uma conta gratuita
- Copie seu token do dashboard
Passo 1: Estrutura Básica
Vamos criar a estrutura do nosso screener:
# stock_screener.py
import requests
import pandas as pd
from dataclasses import dataclass
from typing import Optional
import time
@dataclass
class ScreenerCriteria:
"""Critérios de filtragem do screener"""
# Valuation
max_pl: Optional[float] = None # P/L máximo
min_pl: Optional[float] = None # P/L mínimo (evitar negativos)
max_pvp: Optional[float] = None # P/VP máximo
max_psr: Optional[float] = None # P/Receita máximo
# Rentabilidade
min_roe: Optional[float] = None # ROE mínimo (%)
min_roic: Optional[float] = None # ROIC mínimo (%)
min_margem_liquida: Optional[float] = None # Margem líquida mínima (%)
# Dividendos
min_dividend_yield: Optional[float] = None # DY mínimo (%)
# Endividamento
max_divida_pl: Optional[float] = None # Dívida/PL máximo
# Liquidez
min_volume_diario: Optional[float] = None # Volume mínimo diário
# Setor
setores: Optional[list] = None # Lista de setores permitidos
excluir_setores: Optional[list] = None # Setores a excluir
class BrapiScreener:
"""Stock Screener usando a API brapi"""
def __init__(self, token: str):
self.token = token
self.base_url = "https://brapi.dev/api"
self.all_stocks = []
self.filtered_stocks = []
def get_all_tickers(self) -> list:
"""Obtém lista de todas as ações disponíveis"""
url = f"{self.base_url}/quote/list"
params = {"token": self.token}
response = requests.get(url, params=params)
data = response.json()
# Filtra apenas ações (exclui FIIs, ETFs, BDRs por padrão)
stocks = [
item['stock']
for item in data.get('stocks', [])
if item.get('type') == 'stock'
]
print(f"📋 {len(stocks)} ações encontradas na B3")
return stocks
def get_stock_data(self, tickers: list, batch_size: int = 20) -> list:
"""Obtém dados fundamentalistas das ações em lotes"""
all_data = []
# Processa em lotes para não sobrecarregar a API
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i + batch_size]
batch_str = ','.join(batch)
url = f"{self.base_url}/quote/{batch_str}"
params = {
"token": self.token,
"modules": "defaultKeyStatistics,financialData,summaryProfile"
}
try:
response = requests.get(url, params=params, timeout=30)
data = response.json()
if 'results' in data:
all_data.extend(data['results'])
# Progresso
progress = min(i + batch_size, len(tickers))
print(f"⏳ Processando: {progress}/{len(tickers)} ações...")
# Rate limiting
time.sleep(0.5)
except Exception as e:
print(f"❌ Erro no lote {i}: {e}")
continue
return all_data
def apply_filters(self, stocks: list, criteria: ScreenerCriteria) -> list:
"""Aplica os filtros definidos nos critérios"""
filtered = []
for stock in stocks:
if self._matches_criteria(stock, criteria):
filtered.append(stock)
return filtered
def _matches_criteria(self, stock: dict, criteria: ScreenerCriteria) -> bool:
"""Verifica se uma ação atende aos critérios"""
# Extrai dados com valores padrão
stats = stock.get('defaultKeyStatistics', {})
financial = stock.get('financialData', {})
profile = stock.get('summaryProfile', {})
pl = stats.get('trailingPE')
pvp = stats.get('priceToBook')
roe = financial.get('returnOnEquity')
roic = financial.get('returnOnAssets') # Aproximação
dy = stats.get('dividendYield')
margem = financial.get('profitMargins')
volume = stock.get('regularMarketVolume')
setor = profile.get('sector', '')
# Aplica filtros (None = ignorar critério)
# P/L
if criteria.max_pl is not None:
if pl is None or pl <= 0 or pl > criteria.max_pl:
return False
if criteria.min_pl is not None:
if pl is None or pl < criteria.min_pl:
return False
# P/VP
if criteria.max_pvp is not None:
if pvp is None or pvp <= 0 or pvp > criteria.max_pvp:
return False
# ROE
if criteria.min_roe is not None:
if roe is None or (roe * 100) < criteria.min_roe:
return False
# Dividend Yield
if criteria.min_dividend_yield is not None:
if dy is None or (dy * 100) < criteria.min_dividend_yield:
return False
# Margem Líquida
if criteria.min_margem_liquida is not None:
if margem is None or (margem * 100) < criteria.min_margem_liquida:
return False
# Volume
if criteria.min_volume_diario is not None:
if volume is None or volume < criteria.min_volume_diario:
return False
# Setores
if criteria.setores is not None:
if setor not in criteria.setores:
return False
if criteria.excluir_setores is not None:
if setor in criteria.excluir_setores:
return False
return True
def run(self, criteria: ScreenerCriteria) -> pd.DataFrame:
"""Executa o screener completo"""
print("\n🔍 INICIANDO STOCK SCREENER")
print("=" * 50)
# 1. Busca todas as ações
tickers = self.get_all_tickers()
# 2. Obtém dados fundamentalistas
print("\n📊 Buscando dados fundamentalistas...")
stocks_data = self.get_stock_data(tickers)
# 3. Aplica filtros
print("\n🎯 Aplicando filtros...")
filtered = self.apply_filters(stocks_data, criteria)
# 4. Converte para DataFrame
df = self._to_dataframe(filtered)
print(f"\n✅ {len(df)} ações encontradas de {len(stocks_data)} analisadas")
return df
def _to_dataframe(self, stocks: list) -> pd.DataFrame:
"""Converte resultados para DataFrame formatado"""
rows = []
for stock in stocks:
stats = stock.get('defaultKeyStatistics', {})
financial = stock.get('financialData', {})
profile = stock.get('summaryProfile', {})
rows.append({
'Ticker': stock.get('symbol', ''),
'Nome': stock.get('shortName', ''),
'Preço': stock.get('regularMarketPrice', 0),
'P/L': stats.get('trailingPE', 0),
'P/VP': stats.get('priceToBook', 0),
'ROE (%)': (financial.get('returnOnEquity', 0) or 0) * 100,
'DY (%)': (stats.get('dividendYield', 0) or 0) * 100,
'Margem (%)': (financial.get('profitMargins', 0) or 0) * 100,
'Volume': stock.get('regularMarketVolume', 0),
'Setor': profile.get('sector', ''),
})
df = pd.DataFrame(rows)
# Ordena por Dividend Yield
if not df.empty:
df = df.sort_values('DY (%)', ascending=False)
return dfPasso 2: Usando o Screener
Exemplo 1: Value Investing (Graham/Bazin)
Critérios clássicos de value investing:
# Screener Value Investing
if __name__ == "__main__":
# Seu token da brapi
TOKEN = "SEU_TOKEN_AQUI"
# Inicializa o screener
screener = BrapiScreener(TOKEN)
# Define critérios Value Investing
criterios_value = ScreenerCriteria(
max_pl=15, # P/L abaixo de 15
min_pl=0, # P/L positivo (lucro)
max_pvp=1.5, # P/VP abaixo de 1.5
min_roe=10, # ROE acima de 10%
min_dividend_yield=4, # DY acima de 4%
)
# Executa
resultados = screener.run(criterios_value)
# Exibe resultados
print("\n📊 AÇÕES VALUE INVESTING")
print(resultados.to_string(index=False))
# Salva em CSV
resultados.to_csv('acoes_value.csv', index=False)
print("\n💾 Resultados salvos em acoes_value.csv")Exemplo 2: Dividend Growth (Crescimento de Dividendos)
# Screener Dividendos
criterios_dividendos = ScreenerCriteria(
min_dividend_yield=6, # DY acima de 6%
max_pl=12, # P/L razoável
min_roe=12, # Empresa rentável
min_margem_liquida=10, # Margem saudável
)
resultados = screener.run(criterios_dividendos)
print("\n💰 MELHORES PAGADORAS DE DIVIDENDOS")
print(resultados.to_string(index=False))Exemplo 3: Screener Setorial (Bancos)
# Screener Setor Bancário
criterios_bancos = ScreenerCriteria(
setores=['Financial Services'],
max_pl=10,
min_roe=12,
min_dividend_yield=5,
)
resultados = screener.run(criterios_bancos)
print("\n🏦 MELHORES BANCOS PARA INVESTIR")
print(resultados.to_string(index=False))Exemplo 4: Small Caps Subvalorizadas
# Screener Small Caps
criterios_small = ScreenerCriteria(
max_pl=8, # Muito baratas
max_pvp=1.0, # Abaixo do valor patrimonial
min_roe=15, # Mas rentáveis
min_volume_diario=100000, # Liquidez mínima
)
resultados = screener.run(criterios_small)
print("\n🔍 SMALL CAPS SUBVALORIZADAS")
print(resultados.to_string(index=False))Passo 3: Estratégias Avançadas
Magic Formula (Joel Greenblatt)
A Magic Formula combina empresas baratas E de qualidade:
class MagicFormulaScreener(BrapiScreener):
"""Implementação da Magic Formula de Joel Greenblatt"""
def run_magic_formula(self, top_n: int = 30) -> pd.DataFrame:
"""Executa a Magic Formula e retorna as top N ações"""
# Busca todas as ações
tickers = self.get_all_tickers()
stocks_data = self.get_stock_data(tickers)
# Calcula rankings
rankings = []
for stock in stocks_data:
stats = stock.get('defaultKeyStatistics', {})
financial = stock.get('financialData', {})
# Earnings Yield (inverso do P/L) = quanto maior, melhor
pl = stats.get('trailingPE')
earnings_yield = (1 / pl * 100) if pl and pl > 0 else 0
# Return on Capital = ROE como proxy
roc = (financial.get('returnOnEquity') or 0) * 100
if earnings_yield > 0 and roc > 0:
rankings.append({
'symbol': stock.get('symbol'),
'name': stock.get('shortName'),
'price': stock.get('regularMarketPrice'),
'earnings_yield': earnings_yield,
'roc': roc,
'pl': pl,
})
df = pd.DataFrame(rankings)
if df.empty:
return df
# Rank por Earnings Yield (maior = melhor = rank menor)
df['rank_ey'] = df['earnings_yield'].rank(ascending=False)
# Rank por ROC (maior = melhor = rank menor)
df['rank_roc'] = df['roc'].rank(ascending=False)
# Magic Formula = soma dos ranks (menor = melhor)
df['magic_score'] = df['rank_ey'] + df['rank_roc']
# Ordena pelo score
df = df.sort_values('magic_score').head(top_n)
# Formata saída
result = df[['symbol', 'name', 'price', 'pl', 'earnings_yield', 'roc', 'magic_score']]
result.columns = ['Ticker', 'Nome', 'Preço', 'P/L', 'Earnings Yield', 'ROC', 'Score']
return result.reset_index(drop=True)
# Uso
screener = MagicFormulaScreener(TOKEN)
magic_portfolio = screener.run_magic_formula(top_n=20)
print("\n🎩 MAGIC FORMULA - TOP 20")
print(magic_portfolio.to_string(index=False))Piotroski F-Score
Indicador de qualidade financeira (0-9 pontos):
def calculate_piotroski_score(stock: dict) -> int:
"""Calcula o F-Score de Piotroski (0-9)"""
score = 0
stats = stock.get('defaultKeyStatistics', {})
financial = stock.get('financialData', {})
# 1. ROA positivo
roa = financial.get('returnOnAssets', 0) or 0
if roa > 0:
score += 1
# 2. Fluxo de caixa operacional positivo
ocf = financial.get('operatingCashflow', 0) or 0
if ocf > 0:
score += 1
# 3. ROA crescente (precisaria de histórico)
# Simplificado: ROA > 5% = bom
if roa > 0.05:
score += 1
# 4. Qualidade dos lucros (FCO > Lucro)
net_income = financial.get('netIncomeToCommon', 0) or 0
if ocf > net_income:
score += 1
# 5-9: Precisariam de dados históricos comparativos
# Para simplificar, usamos proxies
# 5. Dívida reduzindo (Dívida/PL < 1 = bom)
debt_equity = financial.get('debtToEquity', 999) or 999
if debt_equity < 100: # < 1 em percentual
score += 1
# 6. Liquidez (current ratio > 1)
current_ratio = financial.get('currentRatio', 0) or 0
if current_ratio > 1:
score += 1
# 7. Sem diluição (simplificado: P/VP < 2)
pvp = stats.get('priceToBook', 999) or 999
if pvp < 2:
score += 1
# 8. Margem bruta positiva
gross_margin = financial.get('grossMargins', 0) or 0
if gross_margin > 0.2: # > 20%
score += 1
# 9. Giro do ativo (simplificado: receita > 0)
revenue = financial.get('totalRevenue', 0) or 0
if revenue > 0:
score += 1
return score
# Adiciona ao screener
def run_piotroski(self) -> pd.DataFrame:
"""Screener com Piotroski F-Score"""
tickers = self.get_all_tickers()
stocks_data = self.get_stock_data(tickers)
results = []
for stock in stocks_data:
score = calculate_piotroski_score(stock)
if score >= 7: # Apenas ações com score alto
results.append({
'Ticker': stock.get('symbol'),
'Nome': stock.get('shortName'),
'Preço': stock.get('regularMarketPrice'),
'F-Score': score,
})
df = pd.DataFrame(results)
return df.sort_values('F-Score', ascending=False)Passo 4: Automação e Alertas
Rodar Diariamente com Cron
# screener_diario.py
import schedule
import time
from datetime import datetime
def executar_screener():
"""Executa o screener e salva resultados"""
print(f"\n⏰ Executando screener às {datetime.now()}")
screener = BrapiScreener(TOKEN)
criterios = ScreenerCriteria(
max_pl=12,
min_roe=12,
min_dividend_yield=5,
)
resultados = screener.run(criterios)
# Salva com data
filename = f"screener_{datetime.now().strftime('%Y%m%d')}.csv"
resultados.to_csv(filename, index=False)
print(f"💾 Salvo em {filename}")
# Envia alerta se encontrar novas oportunidades
if len(resultados) > 0:
enviar_alerta(resultados)
def enviar_alerta(df):
"""Envia notificação com resultados"""
# Implementar: email, Telegram, Discord, etc.
print(f"📧 {len(df)} oportunidades encontradas!")
# Agenda para rodar às 18:00 (após fechamento do mercado)
schedule.every().day.at("18:00").do(executar_screener)
print("🤖 Screener automático iniciado...")
while True:
schedule.run_pending()
time.sleep(60)Exportar para Excel com Formatação
def exportar_excel(df: pd.DataFrame, filename: str):
"""Exporta para Excel com formatação profissional"""
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Screener', index=False)
# Formata a planilha
worksheet = writer.sheets['Screener']
# Ajusta largura das colunas
for column in worksheet.columns:
max_length = max(len(str(cell.value)) for cell in column)
worksheet.column_dimensions[column[0].column_letter].width = max_length + 2
print(f"📊 Exportado para {filename}")
# Uso
exportar_excel(resultados, 'screener_resultado.xlsx')Exemplos de Saída
Screener Value Investing
📊 AÇÕES VALUE INVESTING
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Ticker Nome Preço P/L P/VP ROE(%) DY(%)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BBAS3 BANCO DO BRASIL 52.30 4.2 0.8 18.5 8.2
CMIN3 CSN MINERAÇÃO 5.80 5.1 1.2 28.3 12.1
BBSE3 BB SEGURIDADE 35.40 8.9 1.4 19.7 7.8
TAEE11 TAESA 35.20 7.8 1.3 22.1 9.5
CMIG4 CEMIG 12.50 6.2 0.9 15.2 8.9
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Magic Formula Top 10
🎩 MAGIC FORMULA - TOP 10
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Ticker Nome P/L EY(%) ROC(%) Score
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 CMIN3 CSN MINERAÇÃO 5.1 19.6 28.3 12
2 BBAS3 BANCO DO BRASIL 4.2 23.8 18.5 15
3 GOAU4 GERDAU MET 6.8 14.7 21.2 18
4 GGBR4 GERDAU 5.9 16.9 19.8 21
5 CSNA3 CSN 4.5 22.2 16.1 24
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Código Completo
O código completo está disponível para download:
# Arquivo: stock_screener_completo.py
# GitHub: github.com/brapi-dev/examples/stock-screener
# Instalação:
# pip install requests pandas openpyxl schedule
# Uso:
# python stock_screener_completo.py --token SEU_TOKEN --strategy valuePróximos Passos
- Crie sua conta gratuita - 15.000 requisições/mês
- Veja a documentação da API - Todos os endpoints disponíveis
- Teste no playground - Experimente antes de programar
- Tutorial de Backtesting - Teste suas estratégias
FAQ
Quantas requisições o screener usa?
Depende do número de ações. Para ~400 ações em lotes de 20:
- ~20 requisições para dados
- Cabe no plano gratuito (15.000/mês) rodando ~25x/mês
Posso incluir FIIs no screener?
Sim! Modifique o filtro em get_all_tickers():
if item.get('type') in ['stock', 'fund']: # Inclui FIIsOs dados são em tempo real?
Depende do seu plano:
- Gratuito: 30 min delay
- Startup: 15 min delay
- Pro: 5 min delay
Para screener fundamentalista, isso é mais que suficiente.
Automatize sua busca por ações com a brapi.dev. Comece gratuitamente agora!
