Guide · Backtest
Total return on B3 with Dados B3: what the data gives you and what you still build
The data gives you the session price, the distributions and the corporate actions of each stock, each with its source. What does not come ready is the adjusted series or the total return: that calculation is yours. This guide shows the recipe, where it needs a decision from you and the check that closes it.
What each route delivers
| Route | What it returns | Adjusted? |
|---|---|---|
/empresas/{T}/precos | Daily series (open, high, low, close, volume) in one call; de/ate and formato=csv | No |
/empresas/{T}/eventos | Splits, reverse splits, bonus issues and spin-offs: source, declared factor, net effect of the day, what the price said | — |
/empresas/{T}/dividendos | Distributions with record date and amount per share, gross of income tax on JCP | On the basis of the time |
/empresas/{T}/multiplos | Point-in-time multiples, priced at the first session after the balance sheet was published | No |
All of them need a key, except on the open tickers; the free one works (200 queries a day, no card). The whole series of a stock is one query.
The recipe, in five steps
import json, os, urllib.request
def get(rota):
req = urllib.request.Request("https://dadosb3.com" + rota,
headers={"X-API-Key": os.environ["DADOS_B3_API_KEY"]})
return json.load(urllib.request.urlopen(req))
T = "WEGE3"
precos = get(f"/empresas/{T}/precos")["serie"] # 1 call: the whole series
eventos = get(f"/empresas/{T}/eventos")["eventos"]
proventos = get(f"/empresas/{T}/dividendos")["proventos"]
# 1) one factor per event day: the net effect is already the one of the day's group of events
fatores = {}
for e in eventos:
if e["cisao"] or not e["aplicado_no_ajuste"] or e["efeito_liquido_do_dia"] is None:
continue
fatores[e["data_ex"]] = e["efeito_liquido_do_dia"]
def fator_depois(data):
"""product of the events still to happen from this day on (data_ex >= data)"""
f = 1.0
for d, x in fatores.items():
if d >= data:
f *= x
return f
# 2) adjusted close: the price from BEFORE the event is divided by the net effect
datas = [p["data"] for p in precos]
fech = {p["data"]: p["fechamento"] / fator_depois(p["data"]) for p in precos}
# 3) distribution: goes ex on the session after the record date, adjusted like the price
ex = {}
for p in proventos:
if p["tipo_acao"] != "ON" or not p["data_com"]:
continue
seguinte = next((d for d in datas if d > p["data_com"]), None)
if seguinte:
ex[seguinte] = ex.get(seguinte, 0.0) + p["valor"] / fator_depois(p["data_com"])
# 4) daily total return and cumulative index (base 1.0; distributions reinvested at the day's close)
indice, anterior = {}, None
acum = 1.0
for d in datas:
if anterior:
acum *= (fech[d] + ex.get(d, 0.0)) / fech[anterior]
indice[d], anterior = acum, d
# 5) check: the largest daily jump LEFT after the adjustment
saltos = sorted(((fech[b] / fech[a] - 1, b) for a, b in zip(datas, datas[1:])), key=lambda s: abs(s[0]))
print(saltos[-3:])
Steps 1 and 2: the price from before an event is divided by its net effect, and the adjustment is cumulative. Step 3: a distribution declared before a split is in the share count of its time, so it gets the same adjustment as the price. Step 4: the day's return is the close plus the distribution that goes ex that day, over the previous close. Step 5 is the check, explained below.
Two details that change the number
The event date is the last day WITH rights. The data_ex field carries the exchange's lastDatePrior: the price drops on the next session. That is why the adjustment divides the rows with data <= data_ex, as in the code.
Events on the same day are one operation. The exchange records each leg as a separate row. For TIMS3, on 02/07/2025, there is a reverse split of 0.01x and a split of 100x approved on 02/06/2025: together they are 1.00x and the price did not jump (it closed at 21.96 and 22.60 on the two sessions around it). Applying one leg alone would create a hundredfold jump. That is why the route carries efeito_liquido_do_dia, the product of the group, and the code above uses that field.
What stays your decision
- JCP. The amount in the data is gross, before the tax withheld on JCP. Whether and how much to deduct is an assumption of your study: record it.
- Where to reinvest. The recipe reinvests at the close of the day the stock starts trading without the distribution. That is one convention, not the only one.
- Spin-offs. When the event issues an asset different from the original, the route flags
cisao: trueand the factor adjustment does not apply: the code above skips them. What to do with the asset received is your decision. - Events only we found. Those with
fonte: inferido_capital_e_precoare left out of the automatic adjustment (aplicado_no_ajuste: false): the share count changed and the price jumped, but the exchange does not publish the event. Decide case by case.
Check before you trust
After the adjustment, look at the largest daily jump left. For WEGE3, the split of 28/04/2021 shows as a 50.7% drop in the raw price and disappears in the adjusted one; the largest jump left is -20.6%, on 16/03/2020, a real market fall. A jump left with no market explanation deserves a primary source. An example in the data itself: ITUB4 fell 18.0% on 04/10/2021 (from 29.67 to 24.34) and the data has no event for it in 2021. A fall of that size with no recorded event usually points to a separation or distribution that the data does not capture: the automatic adjustment does not fix it, and the researcher has to decide how to treat it.
Events whose situation is not confirmado_pelo_preco also deserve a look: divergente_do_preco means the declared factor does not explain the observed drop, and the stock leaves the calculation for that period.
What the data does not do
- Intraday. Prices are end of day.
- Indices. IBOV and other indices are not in the data; download the series from the exchange. Historical index composition is not either.
- Historical universe. The data has the companies listed today; those that left the exchange have only part of their history. The size of the bias is at /saiu-da-bolsa. A historical selection on this universe has survivorship bias.
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-11-09-ancora-do-fre · page generated 2026-11-10 02:32 UTC. If this does not match /saude, you are reading a cached copy.