Guide · Python
A B3 stock screener in Python, without looking into the future
The most common mistake when testing an investment idea on financial statements is using a number that did not exist yet on the test date. The 2018 statement only reaches the regulator in 2019 — on average 100 days after year end. This guide screens stocks by what was public on each date, and keeps the result in a form someone else can verify.
Install
pip install dadosb3
The screen covers the whole universe, so it needs a key — the free one works (200 queries a day, no card). Put it in DADOS_B3_API_KEY or pass it to the constructor.
Two different questions: ano and as_of
from dadosb3 import DadosB3
api = DadosB3() # reads DADOS_B3_API_KEY
# "who had ROIC above 15% IN financial year 2018"
by_year = api.screener(roic_min=0.15, ano=2018)
# "what could I have picked on 2019-06-30"
by_date = api.screener(roic_min=0.15, dl_ebitda_max=2, as_of="2019-06-30")
for e in by_date["empresas"]:
print(e["ticker"], e["ano"], e["disponivel_em"], e["indicadores"]["roic"])
The first answers a question nobody could answer in 2018. The second uses, for each company, the latest year whose statement had already reached the regulator on that date — and each row carries disponivel_em so you can check. Indicators are fractions: 15% ROIC is 0.15. An invalid filter name is rejected with an error, never ignored; the full menu is at /screener.
Keeping the answer: the receipt
Next week, after the database updates, the same question may return another list. To cite today's, ask for a receipt:
r = api.screener(roic_min=0.15, as_of="2019-06-30", recibo=1)
print(r["recibo"]["url"]) # permanent address
print(r["recibo"]["sha256"]) # fingerprint of the result
The receipt stores the question, the data version and the result, and opens without a key — whoever gets the link can check it. The same question on the same version returns the same receipt. To verify the fingerprint:
import hashlib, json, urllib.request
rec = json.load(urllib.request.urlopen(r["recibo"]["json"]))
body = json.dumps(rec["resultado"], sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode()
assert hashlib.sha256(body).hexdigest() == rec["sha256"]
The limit this does not fix
The as-of date removes the future from the numbers, not from the universe: the database holds the companies active today. A 2019 query picks only among those that survived until now — survivorship bias. The response says so in limite_conhecido, and the size of the hole is measured company by company at /saiu-da-bolsa. That is why we do not publish portfolio backtests: with only 11% of the companies that left reconstructible, the result would mislead.
Sources: CVM (open data, ODbL) and B3 (COTAHIST). Not affiliated with B3 or the CVM. Not investment advice.
Numbers on this page are live. Data version 2026-09-14-etfs · page generated 2026-09-23 00:39 UTC. If this does not match /saude, you are reading a cached copy.