Dados B3 › Transparency
Transparency
Transparency — how you check the numbers yourself
Data version 2026-10-01-cfo-metodo-direto · page generated 2026-10-13 16:29 UTC. If these do not match /saude, you are reading a cached copy.
What is checked, before every publication — 653 automated tests, collected live
38 are DATA invariants (does the number tie out?) and 124 are product tests — access gate, navigation, language, AI connector. Calling all 653 'invariants' would be inflating it: the split is checkable in the list itself. Not a hand-picked highlight reel: every test below was found by introspecting the actual test files just now, the same way pytest --collect-only would. Grouped by what it protects against, not by which file it lives in.
Level 1 — Integrity: does the data tie out? (38)
I-H09— Raw downloads never land inside `_build/`, the directory wiped at the start of every rebuild. This cost hours: the rebuild runs a subprocess with DADOS_DIR pointing at _build, and without ZIPS_DIR declared the naive default put ~3GB of raw files exactly there — every restart erased them and re-downloaded from scratch, so no attempt ever finished. The environment variable fixes it, but depending on one is fragile: deleting it was what created the loop. This test makes the DEFAULT safe, so configuration is a tweak and not a requirement.I-H10— The rebuild declares TMPDIR and SQLITE_TMPDIR outside /tmp. SQLite writes temporary files when creating indexes and sorting large tables, looking for a directory in this order: SQLITE_TMPDIR, TMPDIR, /tmp. Neither of the first two existed, so it fell through to /tmp — capped at 2GB on Render. The build died at the very end every time, right after ~550k REIT prices, during index creation: instance killed, restart, full ingestion again, never finishing. The first suspicion — the raw downloads — was wrong: moving those to disk changed nothing, because they were never the consumer at that stage.I-H11— A database missing the derived tables gains them at BOOT, not through a full rebuild. Covenants and the excluded-companies list depend on nothing the rebuild recomputes: they come from files versioned in the repository. Taking the official route of bumping the data version would cost around twenty-six minutes of reconstruction in a build that has already died of memory exhaustion and, on one occasion, took down an external audit in progress. The test loads into an empty database and checks it loaded, then runs again and checks it did NOT reload, because overwriting what a build placed would be the boot overruling the build's authority.I-H12— A failure loading a derived file becomes a log warning, never an exception at boot. One page missing is bad; the whole site being down is worse, and this product has already been unavailable precisely to the people trying to audit it. The attack forces ingestion to blow up and requires that the service still comes up, by checking that the caller wraps the load in a try/except rather than trusting it.I-H01— The flow case of 21/08: the external source returned ZERO and the database under construction was left with no rows at all.I-H02— The FX case of 21/08 — the one a `if it came back empty: copy` would let through, because it came back half-full, not empty.I-H03— The rule in one sentence: after inheritance, no table may hold FEWER rows than the previous database.I-H04— First build ever (or a wiped disk): there is nothing to inherit from, and that must not raise.I-H05— The REAL numbers of 21/08/2026 have to be refused.I-H06— The gate must NOT be zero, and this is the test that prevents it.I-H07— The normal case: a bigger new database. It passes.I-H08— First build ever: there is nothing to compare against, so it releases.I-H13— A new rule may legitimately remove rows (the restatement key gained the aggregation level and 9,467 rows became 8,654), but only by DECLARING how much, per data version, and only at the version switch: the same declaration does not license the next build of the same version to shrink again, a declaration for one count does not release another, and the drop cannot exceed what was declared. Without this, the anti-shrink guard refused the first rebuild in production and FII/ETF pages stayed unavailable.I-H14— The daily backup copies the three small databases on the persistent disk (follow-up requests, analytics, receipts) through SQLite’s backup API into DADOS_DIR/backup/YYYY-MM-DD/, whole and openable; it does not copy again on the same day; a missing database is left out; only the 7 most recent days are kept; /saude gives the date of the last one and how many files, with no path or content. Everything stays inside DADOS_DIR.I-H15— The finished database goes up in chunks through /admin/base/*, only with the X-Admin-Token header (the URL query and a missing header answer 404), and is parked with the version it carries, without touching the production database. Built on the owner's computer because rebuilding inside the 2 GB container took the site down for lack of memory.I-H16— A chunk with the wrong offset gets 409 with the current size (so the script can resume after a connection drop); empty or oversized chunks and a second upload while a base is ready are refused; a wrong sha256, invalid gzip, or a file that is not an intact database with a data_version fail WITH a reason and delete the partial file so the upload restarts clean.I-H17— The parked database replaces the production one only when it carries the version the running CODE promises, production is not on it yet, and no watched count shrank beyond the limit. A different version WAITS (the base stays); a shrink REFUSES with the reason on its record (the base stays and production is untouched).I-H18— With RECONSTRUIR_NO_SERVIDOR=0 the server does not rebuild the database (new version or Sunday) but still rebuilds when there is no database at all, the rescue for an empty disk; without the variable the old behaviour holds, and the daily update keeps running in the same loop.I-H19— The local script uploads the compressed database in chunks to /admin/base/*, resumes from where the server stopped (without resending what already arrived), ends with the base READY and never writes the admin token to its output.I-H20— /saude reports the process and container memory as the Linux host sees it (resident, peak resident, container including file cache, and the limit), and omits the block where it does not exist. A memory-limit restart on the host could not be explained without it.I-H21— The NAS archive step copies only what can be rebuilt (public CVM/B3 zips, the compressed finished base and the logs), with house-style names, never subscriber databases or credentials, never deletes anything, does not recopy what is already there, and an unreachable NAS only warns instead of failing the upload.I-H22— Checking a parked base (sha256, decompression, quick_check) asks Linux to drop the file cache of the partial upload and of the decompressed database; where the call does not exist (Windows) it is a no-op. The host container counts file cache against its 2 GB memory limit.I-H23— While a parked base is decompressed, checked or received, a helper thread asks Linux to drop the file cache every second (and after every received chunk), and it stops when the block ends. A real-size rehearsal measured the host container touching its 2,048 MB limit through file cache; dropping only at the end of each step was not enough.I-H24— Decompressing a parked base writes and cleans in the same loop: every fixed number of bytes written it forces the write to disk (fsync) and asks Linux to drop the file cache, so the dirty cache never exceeds that amount. A real-size rehearsal still took the host container to its 2,048 MB limit for ~25 s with a once-per-second helper thread, because dirty pages cannot be dropped before they are written.I-H25— If a network query times out in the local script (in the end-of-upload notice, whose reply is lost though the server already started checking, or while waiting for the check) it retries instead of dying and ends with the base READY; a 409 already-in-progress on the repeated notice counts as arrived; with no answer at all it gives up with a message instead of retrying forever. A real-size rehearsal had the base ready on the server while the script crashed on a read timeout.I-H26— The parked-base state endpoint tells whether the server still rebuilds the database (reading RECONSTRUIR_NO_SERVIDOR: true without the variable or with 1, false with 0), which is how the owner confirms from outside that the variable took effect on the host after the restart.I-H27— The local script's --help finishes and prints its text even when the console only understands cp1252 (the Windows one): characters it cannot print are replaced instead of crashing the script. The help text has a star glyph and --help crashed with UnicodeEncodeError on the owner's console.I-H28— On activation the parked base receives from the production database the foreign-flow, exchange-rate, CPI and Selic rows it lacks (INSERT OR IGNORE: what the new base brought stays), and only then does the shrink guard decide. The first local weekly rebuild could not reach the central bank API (NXDOMAIN on every DNS) and brought zero rows; the server refused the swap and two hours of work stalled. The old server build inherited from the previous database; the new flow did not.I-H29— On activation the ETF daily-report and monthly-balance tables receive from the production database the rows the parked base lacks; on a key conflict the row with the HIGHER document id wins (the schema rule); the amendment history only gains what is missing; a table absent on either side is skipped without failing the swap. The FNET backfill has a per-run time budget and only the live database holds everything fetched so far: the first local rebuild had 24% fewer daily reports and no guard watches that count.I-H30— Swapping the parked base keeps the database that was live as `<database>.anterior` (a hard link: no 2.3 GB copy); the next swap replaces it; `reverter` puts the previous one back with the same WAL hygiene and consumes it; without a previous one it answers 404. The first swap deleted the old database and with it ~3,500 ETF daily reports that only it had.I-H31— GET /admin/base/ar streams the live database as gzip that decompresses to the IDENTICAL file, with original size and data version in headers; only with the owner header (404 without it or with the token in the URL); 503 when the WAL has unconsolidated frames and 409 when a download is already running; the lock is released.I-H32— The local script downloads the live database into the NAS before it is replaced, checking the stream while it downloads (decompress and compare with the original size) and writing bases/ar/<date>_ar_<version> plus the sha256 only if it closes; a cut stream never enters the NAS and is retried; an unreachable NAS or silent server only warns and never fails the upload; the token is never echoed.I-H33— The redundant NAS only acts when DADOSB3_NAS_REDUNDANTE is set: it then receives the house files and mirrors the live-database copies already in the main NAS, without re-copying or copying subscriber databases; with the new disk offline it only warns and never stops the build.I-H34— The local script's production gate always runs pytest with PYTEST_DEBUG_TEMPROOT in its own folder inside the build folder (created if missing, overriding any inherited value), and when pytest exits with an error it tells failed tests (FAILED/ERROR lines or a summary with failures) apart from a pytest crash after the tests (neither). On 12/10/2026 every test passed, pytest died cleaning the shared Windows temp folder, and the message said the gate failed with an empty list.I-H35— Between the parked-base file appearing and the record with its version being written, the state answers "processing", never "ready" with an empty version; the local script and the restart only read "ready" once the record says which version arrived. The check swapped the file first and wrote the record after it, and a reader in that window saw "ready" with no version, which made I-H25 fail intermittently.I-H36— On Linux, importing the API caps the memory allocator at 2 arenas and the hourly loop hands freed memory back to the system; elsewhere nothing is called and the state says so. The pool of synchronous routes is capped at 8 threads, and /saude shows threads, arenas and returns. Added on 12/10/2026 after Render restarted the service for memory while AI agents opened dozens of pages in parallel.I-H37— The production gate of the local build script runs test_navegacao as well (broken link or orphan route stops the weekly build), while the server fallback build keeps it out: the module list in api.py is unchanged, because on the server crawling the whole site next to the new database brought the instance down. If the API module list cannot be read, the gate still refuses to run instead of running navigation alone.I-H38— If the server restarts in the middle of checking a parked database (a deploy), the record still saying "processing" now reads "failed" with the reason, and the same upload can be finalized again until it is ready; while the check really runs in this process, a second finalize is still refused. On 12/10/2026 a merge restarted the server four minutes after a 572 MB database arrived, and it stayed "processing" forever.
Level 2 — Semantics: does the account mean what the calculation assumes? (0)
Level 3 — Economic: is the result possible in the real world? (0)
Other product tests (0)
Access gate, navigation and the MCP connector — they test whether the product works, not whether a number is right, so they don't fit the 3 levels above by design. Still counted, still listed:
Reconstruct it yourself — net margin across 5 companies, 4 sectors
We don't ask you to trust the numbers. Here's the net margin of five companies in four different sectors, each rebuilt straight from its annual report (DFP) at the CVM — net income ÷ revenue, matching the published figure exactly. For ANY indicator, of any company, the whole chain down to the line in CVM's file is at /linhagem:
| Company · sector | Net income (CVM acct) | Revenue (CVM acct) | Margin =÷ |
|---|---|---|---|
| WEGE3 · industrial | R$ 6.78 bn DRE:3.11 | R$ 40.80 bn DRE:3.01 | 16.6% |
| VALE3 · mining | R$ 11.81 bn DRE:3.11 | R$ 213.59 bn DRE:3.01 | 5.5% |
| PETR4 · oil & gas | R$ 110.61 bn DRE:3.11 | R$ 497.55 bn DRE:3.01 | 22.2% |
| ITUB4 · bank | R$ 45.85 bn DRE:3.09 | R$ 387.12 bn DRE:3.01 | 11.8% |
| BBAS3 · bank | R$ 16.78 bn DRE:3.11 | R$ 319.46 bn DRE:3.01 | 5.3% |
Download any of these DFPs from the CVM, take the income and revenue accounts, divide — you get the same number. Banks use interest income as revenue (what makes sense for a bank), so the reconciliation is sector-aware; the others use sales revenue. Figures are the latest fiscal year.
Why we may differ from another site (and it's not an error)
A difference between two sites usually isn't one being wrong — it's a method choice. We disclose ours:
- Average vs ending capital: ROE and ROIC use average equity/capital (this year + last ÷ 2), not the ending balance — so they're not a naive single-year division.
- Controlling vs consolidated: we state which one each item uses.
- TTM vs annual: quarterly multiples use trailing-twelve-months earnings.
- IFRS 16, goodwill, cash, exceptional tax: each handled explicitly and flagged when it distorts.
Do the tests bite? Yes — real cases the suite has caught
A test that never fails could mean perfect data — or a weak test. These were born from real errors that slipped through, and now fail — only shown for tests that actually exist in the collected suite above:
- I-B16 · banks' P/E = 0: the CVM reports share count sometimes in units, sometimes in thousands (varying by company and year); market cap came out 1000× too small and P/E was zero. I-B16 (price × shares recomputed) caught it — we fixed 661 annual and 2,027 quarterly multiples.
- I-B24 · the vanishing profit: the profit/equity account varies by bank (Itaú 3.09, BB 3.11); if the label search fails, the number vanishes silently and ROE is born wrong. I-B24 makes that hole fail.
- H-B23 · the 800% dividend yield: old B3 dividends carry a price not adjusted for splits; without H-B23 the yield would look absurdly real. The flag keeps the record and warns.
- I-B20 · the missing COTAHIST 2023: on the first deploy one year of prices failed to download silently; the API shipped with ~50k fewer prices and the suite passed, because nothing checked coverage. We added I-B20 — a year without prices now fails the build loudly.
- I-B33 · Oil & Gas at −89.6% in one week: COTAHIST isn't split-adjusted; a 10:1 split read as a −90% weekly return and stayed in the cumulative return forever, with nothing failing (a blind amplitude filter didn't fix it either — it just flipped which direction was wrong). I-B33 (no sector week beyond ±35%, the ruler is the exchange itself) stops the build.
- I-B31 · 364 of 865 sector-index weeks frozen: extending the series back to 2010, a sector with no company carrying a market cap that week turned into a factor of 1.0 — a flat line that the cumulative return read as 'market stood still', and the 'since 2010' return came out fictional (+527%). I-B31 fails on any week repeated to the cent.
- I-E05 · EMBRAER's 2010 P/E priced with 2025 data: the point-in-time price lookup fell forward with no ceiling whenever the current ticker had no price at balance-sheet time (ticker change, share-class migration); the multiple came out priced 15 years into the future, in 22% of annual and quarterly rows, silently. The 45-day window (and I-E05) blocks it: no session inside the deadline, no multiple computed.
- I-E04 · AZUL at R$ 1.67 quadrillion market cap: the balance sheet carried 54.7 trillion shares from the judicial recovery issuance, but the price used was already on the post-reverse-split (1:150,000) basis — two ends measured on different bases, market cap wrong by orders of magnitude. I-E04 (market cap never above Brazil's GDP) fails the build on that absurdity.
How far back each block of data goes
No asterisks here: this is coverage year by year, counted right now. Indicators (ROE, ROIC, margins, growth) come from the filings alone and cover the whole series. Multiples (P/E, P/BV, EV/EBITDA) need a price and a share count — and the share count comes from CVM's reference form, which the further back you go the fewer companies filed in a usable format. The gap between the two columns is a source limit, not an ingestion hole. If you look for an old P/E and do not find it, it is because nobody has it — not because we hid it. The same numbers as JSON: /cobertura.
| Year | Companies with indicators | Companies with multiples |
|---|---|---|
| 2025 | 440 | 290 |
| 2024 | 444 | 296 |
| 2023 | 442 | 298 |
| 2022 | 429 | 295 |
| 2021 | 423 | 296 |
| 2020 | 410 | 274 |
| 2019 | 371 | 166 |
| 2018 | 326 | 167 |
| 2017 | 317 | 165 |
| 2016 | 311 | 154 |
| 2015 | 303 | 144 |
| 2014 | 301 | 140 |
| 2013 | 292 | 131 |
| 2012 | 291 | 112 |
| 2011 | 284 | 98 |
| 2010 | 277 | 34 |
Live coverage
456 companies · 878,872 price points · 8,656 recorded restatements · last refresh 2026-10-12 04:46:22. Full live counts at /saude.
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-10-01-cfo-metodo-direto · page generated 2026-10-13 16:29 UTC. If this does not match /saude, you are reading a cached copy.