PT

Dados B3 › Guides › Backtest

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

RouteWhat it returnsAdjusted?
/empresas/{T}/precosDaily series (open, high, low, close, volume) in one call; de/ate and formato=csvNo
/empresas/{T}/eventosSplits, reverse splits, bonus issues and spin-offs: source, declared factor, net effect of the day, what the price said—
/empresas/{T}/dividendosDistributions with record date and amount per share, gross of income tax on JCPOn the basis of the time
/empresas/{T}/multiplosPoint-in-time multiples, priced at the first session after the balance sheet was publishedNo

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

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

Esta página em português

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.