Em 2026, o universo de FIIs na B3 já passa de 1.500 fundos. Olhar um por um na "olho-metro" não escala. Neste tutorial você vai construir, do zero, um screener de FIIs em Python que filtra por P/VP, Dividend Yield, segmento e patrimônio — pronto para rodar todo dia.
Por Que um Screener de FIIs?
Quem investe em FIIs sabe: o mercado é muito heterogêneo. Você tem fundos de R$ 100 milhões e fundos de R$ 10 bilhões; FIIs com 90% em CRI e FIIs com 100% em galpão logístico; DY de 7% e DY de 16%.
Sem uma forma sistemática de filtrar, você acaba caindo em três armadilhas comuns:
- Comprar pelo DY alto isolado — o "yield trap" clássico.
- Pagar caro em P/VP elevado — paga 1.20 numa cota de FII de papel cujo patrimônio é dinheiro.
- Ignorar liquidez e patrimônio — entra num FII pequeno com spread enorme e sai no prejuízo.
Um screener resolve isso aplicando os mesmos critérios numéricos a todos os fundos, todos os dias.
Este tutorial usa exclusivamente os endpoints /api/v2/fii/* da brapi. Eles
são exclusivos do plano Pro, mas você pode testar livremente o código
usando os tickers do sandbox: MXRF11 e HGLG11.
O Que Vamos Construir
Ao final deste artigo, você terá um script Python que:
- Puxa todos os FIIs da B3 via
/api/v2/fii/list - Enriquece com indicadores via
/api/v2/fii/indicators(em lote, até 20 por request) - Aplica filtros configuráveis (P/VP, DY, patrimônio, cotistas)
- Gera um ranking ponderado
- Exporta resultados para CSV / Excel
Tempo estimado: ~30 minutos, mesmo que você esteja começando agora em Python.
Pré-requisitos
1. Ambiente Python
# Crie um diretório de projeto e ambiente virtual
mkdir fii-screener && cd fii-screener
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install requests pandas openpyxl2. Token brapi
Acesse brapi.dev/dashboard, crie sua conta e gere um token. Para os endpoints de FIIs detalhados, é necessário o plano Pro.
# Linux/macOS
export BRAPI_TOKEN="seu_token_aqui"
# Windows PowerShell
# $env:BRAPI_TOKEN="seu_token_aqui"Passo 1: Conhecendo os Endpoints
Antes de codar, vamos entender a estrutura dos dados. A API de FIIs tem 6 endpoints principais — para o screener, vamos usar 2 deles:
GET /api/v2/fii/list
Retorna a lista de FIIs com indicadores resumidos. Aceita filtros:
segmentType:papel,tijolo,hibrido,fofsegmentoAtuacao:Logística,Shoppings,Lajes Corporativas,Títulos e Val. Mob., etc.tipoGestao:AtivaouDefinidamandate:Renda,Ganho de Capital,Híbridosearch: busca textual por nomesortBy,sortOrder,page,limit
curl "https://brapi.dev/api/v2/fii/list?segmentType=tijolo&limit=10&sortBy=dividendYield12m&sortOrder=desc"Resposta abreviada:
{
"fiis": [
{
"symbol": "HGLG11",
"name": "CSHG Logística FII",
"cnpj": "11.728.688/0001-47",
"segmentType": "tijolo",
"segmentoAtuacao": "Logística",
"tipoGestao": "Ativa",
"mandate": "Renda",
"price": 156.05,
"navPerShare": 171.48,
"priceToNav": 0.91,
"dividendYield12m": 0.0843
}
],
"pagination": { "page": 1, "limit": 10, "totalItems": 412, "totalPages": 42 }
}GET /api/v2/fii/indicators
Retorna indicadores fundamentalistas detalhados para FIIs específicos (até 20 símbolos por request):
curl "https://brapi.dev/api/v2/fii/indicators?symbols=MXRF11,HGLG11,XPML11"Resposta abreviada:
{
"fiis": [
{
"symbol": "HGLG11",
"asOfDate": "2025-03-31",
"price": 156.05,
"navPerShare": 171.48,
"priceToNav": 0.91,
"dividendYield12m": 0.0843,
"dividendYield1m": 0.0072,
"monthlyReturn": -0.0032,
"totalInvestors": 488272,
"sharesOutstanding": 53850000,
"equity": 9232380000.00,
"totalAssets": 10147254000.00,
"segmentType": "tijolo",
"segmentoAtuacao": "Logística",
"adminName": "Credit Suisse Hedging-Griffo Cor. de Valores S.A.",
"managerName": "CSHG Logística Fundo de Invest. Imob."
}
]
}Passo 2: Cliente HTTP em Python
Crie um arquivo brapi_fii.py com a camada de acesso à API:
import os
import time
from typing import Iterable
import requests
BASE_URL = "https://brapi.dev/api/v2/fii"
TOKEN = os.getenv("BRAPI_TOKEN", "")
HEADERS = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
# A API aceita até 20 símbolos por request em /indicators
MAX_SYMBOLS_PER_REQUEST = 20
def _get(path: str, params: dict | None = None) -> dict:
"""GET com retry simples para erros transitórios."""
url = f"{BASE_URL}{path}"
for attempt in range(3):
response = requests.get(url, params=params, headers=HEADERS, timeout=30)
if response.status_code == 429:
# Rate limit — espera e tenta de novo
time.sleep(2 ** attempt)
continue
response.raise_for_status()
return response.json()
raise RuntimeError(f"Falha em {url} após 3 tentativas")
def list_all_fiis(
segment_type: str | None = None,
segmento_atuacao: str | None = None,
page_size: int = 100,
) -> list[dict]:
"""Pagina por todos os FIIs do filtro até esgotar os resultados."""
fiis: list[dict] = []
page = 1
while True:
params = {"page": page, "limit": page_size}
if segment_type:
params["segmentType"] = segment_type
if segmento_atuacao:
params["segmentoAtuacao"] = segmento_atuacao
data = _get("/list", params=params)
fiis.extend(data.get("fiis", []))
pagination = data.get("pagination", {})
if not pagination.get("hasNextPage"):
break
page += 1
return fiis
def get_indicators(symbols: Iterable[str]) -> list[dict]:
"""Busca indicadores em lotes de 20."""
symbols = list(symbols)
out: list[dict] = []
for i in range(0, len(symbols), MAX_SYMBOLS_PER_REQUEST):
batch = symbols[i : i + MAX_SYMBOLS_PER_REQUEST]
data = _get("/indicators", params={"symbols": ",".join(batch)})
out.extend(data.get("fiis", []))
return outPontos importantes nesse cliente:
- Paginação automática em
list_all_fiis— você não precisa pensar empagequando chama. - Lote de 20 em
get_indicators— respeita o limite do endpoint. - Retry com backoff para erros 429 (rate limit) e instabilidades pontuais.
Passo 3: Construindo o Screener
Crie screener.py no mesmo diretório:
from dataclasses import dataclass
from datetime import datetime
import pandas as pd
from brapi_fii import get_indicators, list_all_fiis
@dataclass
class FilterConfig:
"""Critérios do screener — ajuste conforme sua estratégia."""
# P/VP máximo aceito (cuidado: papel não tolera P/VP muito acima de 1)
max_price_to_nav: float = 1.05
# DY anual mínimo (12 meses)
min_dividend_yield_12m: float = 0.08 # 8%
# Patrimônio mínimo em R$ (filtro de tamanho/liquidez indireta)
min_equity: float = 500_000_000 # R$ 500 mi
# Mínimo de cotistas (proxy de liquidez e governança)
min_investors: int = 5_000
# Segmentos permitidos (None = todos)
allowed_segments: tuple[str, ...] | None = None
def fetch_fii_universe(segment_type: str | None = None) -> list[str]:
"""Retorna a lista de tickers do segmento solicitado."""
fiis = list_all_fiis(segment_type=segment_type)
return [fii["symbol"] for fii in fiis if fii.get("symbol")]
def build_dataframe(symbols: list[str]) -> pd.DataFrame:
"""Constrói um DataFrame com indicadores fundamentalistas dos FIIs."""
indicators = get_indicators(symbols)
df = pd.DataFrame(indicators)
# Garantir tipos numéricos (algumas APIs retornam string)
numeric_cols = [
"price",
"navPerShare",
"priceToNav",
"dividendYield12m",
"dividendYield1m",
"monthlyReturn",
"totalInvestors",
"sharesOutstanding",
"equity",
"totalAssets",
]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
def apply_filters(df: pd.DataFrame, config: FilterConfig) -> pd.DataFrame:
"""Aplica os filtros do screener."""
mask = (
df["priceToNav"].le(config.max_price_to_nav)
& df["dividendYield12m"].ge(config.min_dividend_yield_12m)
& df["equity"].ge(config.min_equity)
& df["totalInvestors"].ge(config.min_investors)
)
if config.allowed_segments:
mask &= df["segmentType"].isin(config.allowed_segments)
return df[mask].copy()
def rank_by_quality(df: pd.DataFrame) -> pd.DataFrame:
"""Cria um score 0-100 combinando DY, desconto P/VP e tamanho."""
df = df.copy()
# Normaliza cada métrica em rank 0-1 (1 = melhor)
df["rank_dy"] = df["dividendYield12m"].rank(pct=True)
df["rank_discount"] = (1 - df["priceToNav"]).rank(pct=True)
df["rank_size"] = df["equity"].rank(pct=True)
# Score combinado (pesos ajustáveis)
df["score"] = (
df["rank_dy"] * 0.45
+ df["rank_discount"] * 0.35
+ df["rank_size"] * 0.20
) * 100
return df.sort_values("score", ascending=False)Passo 4: Rodando o Screener
Crie run.py:
from datetime import date
from screener import FilterConfig, apply_filters, build_dataframe, fetch_fii_universe, rank_by_quality
def main() -> None:
# 1) Universo: todos os FIIs (ou filtre por segmentType)
print("Listando todos os FIIs...")
symbols = fetch_fii_universe()
print(f" {len(symbols)} FIIs encontrados")
# 2) Indicadores em lote
print("Buscando indicadores fundamentalistas...")
df = build_dataframe(symbols)
print(f" {len(df)} FIIs com indicadores")
# 3) Filtros de qualidade
config = FilterConfig(
max_price_to_nav=1.05,
min_dividend_yield_12m=0.09, # 9%
min_equity=500_000_000,
min_investors=5_000,
allowed_segments=("tijolo", "hibrido"), # Foco em real estate
)
filtered = apply_filters(df, config)
print(f" {len(filtered)} FIIs passaram nos filtros")
# 4) Ranking ponderado
ranked = rank_by_quality(filtered)
# 5) Top 20 + colunas relevantes
cols = [
"symbol",
"segmentType",
"segmentoAtuacao",
"price",
"priceToNav",
"dividendYield12m",
"dividendYield1m",
"equity",
"totalInvestors",
"score",
]
top = ranked[cols].head(20)
print("\nTop 20 do screener:")
print(top.to_string(index=False))
# 6) Exportar
today = date.today().isoformat()
ranked.to_csv(f"fii_screener_{today}.csv", index=False)
ranked.to_excel(f"fii_screener_{today}.xlsx", index=False, engine="openpyxl")
print(f"\nResultados salvos em fii_screener_{today}.csv / .xlsx")
if __name__ == "__main__":
main()Execute:
python run.pySaída esperada (resumida):
Listando todos os FIIs...
1247 FIIs encontrados
Buscando indicadores fundamentalistas...
1247 FIIs com indicadores
38 FIIs passaram nos filtros
Top 20 do screener:
symbol segmentType segmentoAtuacao price priceToNav dividendYield12m ... score
HGLG11 tijolo Logística 156.05 0.91 0.0843 ... 91.4
BTLG11 tijolo Logística 100.10 0.94 0.0901 ... 88.7
...Passo 5: Variações da Estratégia
O screener acima é genérico. Vamos ver três receitas prontas para copiar.
Receita 1: Caçando FIIs de Papel com Margem de Segurança
FIIs de papel não devem ser comprados muito acima de P/VP 1.00 — o patrimônio é dinheiro indexado a IPCA ou CDI, e pagar prêmio sobre dinheiro é furada de longo prazo.
config_papel = FilterConfig(
max_price_to_nav=0.98, # Desconto sobre VP
min_dividend_yield_12m=0.12, # 12%+ DY
min_equity=300_000_000,
min_investors=3_000,
allowed_segments=("papel",),
)Receita 2: FIIs de Logística — High Conviction
Em ciclos de juros caindo, logística costuma performar bem (e-commerce + custo de capital menor):
# Substitua a linha em fetch_fii_universe:
symbols = [
fii["symbol"]
for fii in list_all_fiis(segmento_atuacao="Logística")
if fii.get("symbol")
]
config_logistica = FilterConfig(
max_price_to_nav=1.10, # Tijolo aceita prêmio em ativos premium
min_dividend_yield_12m=0.07,
min_equity=1_000_000_000, # Apenas grandes
min_investors=20_000,
allowed_segments=("tijolo",),
)Receita 3: Renda Mensal Pura (DY 1m alto e estável)
Para quem quer fluxo de caixa agora, foque no DY do último mês ajustado pelo tamanho:
def rank_renda_mensal(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["rank_dy_1m"] = df["dividendYield1m"].rank(pct=True)
df["rank_size"] = df["equity"].rank(pct=True)
df["score"] = (df["rank_dy_1m"] * 0.7 + df["rank_size"] * 0.3) * 100
return df.sort_values("score", ascending=False)Passo 6: Automatize com Cron
Quer rodar o screener todo dia às 19h (após o fechamento do pregão)?
# crontab -e
0 19 * * 1-5 cd /caminho/fii-screener && /caminho/.venv/bin/python run.py >> screener.log 2>&1Ou em Windows com Task Scheduler. Você acumula um histórico de CSVs com a evolução do ranking — útil para identificar FIIs que entraram/saíram da sua "watchlist" recentemente.
Passo 7: Adicionando Histórico de P/VP
O P/VP atual vale pouco sem comparação histórica. Use /api/v2/fii/indicators/history para checar se um FII está realmente "barato":
def historical_pvp_zscore(symbol: str, lookback_months: int = 24) -> float:
"""Z-score do P/VP atual versus mediana dos últimos N meses."""
from brapi_fii import _get
data = _get("/indicators/history", params={"symbols": symbol})
history = data.get("history", [])
pvps = [
row["priceToNav"]
for row in history[-lookback_months:]
if row.get("priceToNav") is not None
]
if len(pvps) < 6:
return 0.0
import statistics
median = statistics.median(pvps)
stdev = statistics.stdev(pvps)
if stdev == 0:
return 0.0
current = pvps[-1]
return (current - median) / stdevZ-score negativo = P/VP atual abaixo da mediana histórica (FII descontado). Combine com o screener para priorizar FIIs que estão simultaneamente:
- Bons fundamentais (passam no
apply_filters) - Baratos historicamente (z-score do P/VP < -0.5)
Cuidados Importantes
P/VP isolado não é tudo
Um FII com P/VP 0.70 pode estar "barato" por um motivo válido — vacância
alta, contratos vencendo, gestão problemática. Sempre leia o relatório
gerencial antes de comprar (/api/v2/fii/reports).
DY 12m é olhar pelo retrovisor
Dividendos passados não são distribuições futuras. FIIs com DY de 18%
frequentemente têm renda extraordinária (venda de imóvel, recebíveis
pontuais) que não se repete. Cheque dividendYield1m e o relatório CVM.
Tamanho e liquidez importam
O número de cotistas (totalInvestors) é um proxy melhor de liquidez do que
o volume diário, que oscila bastante. FIIs com menos de 5.000 cotistas
costumam ter spread bid-ask largo demais para entrada e saída sem custo.
Indo Além
A partir desse esqueleto, você pode evoluir o screener para:
- Alertas automáticos via Telegram quando um novo FII entra na watchlist (combine com bot Telegram)
- Dashboard web com Streamlit ou Next.js
- Backtests comparando o desempenho de carteiras montadas pelo screener vs IFIX
- Análise de relatórios com
/api/v2/fii/reportspara filtrar por composição patrimonial (ex.: FIIs com >70% em CRI High Grade)
Tudo isso fica como exercício para o leitor — a base do dado já está aí.
Conclusão
Um screener é a forma mais escalável de processar 1.500+ FIIs sem fadiga. Com ~150 linhas de Python e a API /api/v2/fii/* da brapi você sai de "qual FII comprar?" para uma shortlist de 20 candidatos com critérios objetivos — todo dia útil.
Comece simples: rode o FilterConfig padrão, exporte para Excel, leia os relatórios dos top 5. Em 1 mês você terá refinado os parâmetros para sua tese e estará operando com muito mais convicção.
Próximos passos: explore os outros endpoints da família
/api/v2/fii/*na documentação oficial, ou veja o guia de FIIs para iniciantes se ainda está se familiarizando com os indicadores.
Disclaimer: este artigo é educacional e não constitui recomendação de investimento. Rentabilidade passada não garante rentabilidade futura. Faça sua própria análise.
