SPEC 005 B3 Import Reconciliation - rafaelfqueiroz/allmywallet GitHub Wiki
| Status | Draft |
| PRD source | F2 (FR-2.4–2.26), §4.3–4.4 (FR-4.1–4.5) |
| Depends on | SPEC-003, SPEC-006 (ledger), SPEC-004 (CPF stripping) |
| Required by | SPEC-020 (onboarding) |
| Milestone | M1 |
Ingestion of the three .xlsx extracts a user exports from investidor.b3.com.br, and reconciliation of the resulting ledger against B3's own position snapshot.
This is the feature the product lives or dies on. B3's Área do Investidor APIs are B2B-only — "Não oferecemos acesso direto às APIs para pessoas físicas" — so file import is how custody data enters the system (PRD §4.1). It must be forgiving of user error, impossible to corrupt through repetition, and transparent about what it did.
All custody ingestion passes through a single ingestion port accepting normalized transaction and position records. The .xlsx parsers are one implementation; manual entry (SPEC-006) is a second; an Open Finance aggregator adapter is a designed-for third.
| ID | Rule |
|---|---|
| BR-005-01 | Three extracts are supported, each with a distinct role: Movimentação (richest source — earnings, splits, subscriptions, transfers, amortisations), Negociação (authoritative trade record with tradeable ticker), Posição (point-in-time snapshot of holdings, one tab per asset class; the source of indexer, issue date and maturity for CDB/LCI/LCA — see BR-005-06). |
| BR-005-02 | Accepts .xlsx and .xls up to the configured size cap (default 10 MB). |
| BR-005-03 | The extract type is identified from file structure, not filename — users rename files. |
| BR-005-04 | Column detection tolerates leading metadata rows and minor header variations, and does not depend on column order. |
| BR-005-05 | An unidentifiable or unparseable file produces a specific, actionable error naming what was expected. Generic failure messages are a defect. |
| BR-005-06 | The Posição Renda Fixa tab is parsed for indexer, issue date and maturity, creating or updating a FixedIncomeContract. B3's export carries no contracted rate, so the rate is typed by the user (from the broker statement) and the contract is valued at cost and flagged until then (SPEC-009 BR-009-13). A later Posição never erases a rate already typed. (Amended 2026-09-14, #108 — see DL-005-08.)
|
| BR-005-06a | A Posição row's asset class comes from its tab: Acoes → stock, Fundo de Investimento → FII, BDR → BDR, ETF → ETF, Tesouro Direto → Tesouro Direto, and on Renda Fixa the Produto prefix (CDB/LCI/LCA). The tab is part of the file's structure, not its name, so BR-005-03 holds. A row on an unknown tab, or a Renda Fixa product that is none of the three, is not imported rather than guessed. (Added 2026-09-14, #108.)
|
| BR-005-07 | CPF is stripped at parse time and never persisted (SPEC-004, BR-004-02). |
| BR-005-08 | No parsing or file-format logic exists outside an ingestion adapter. Everything downstream consumes only the normalized record. |
| ID | Rule |
|---|---|
| BR-005-09 | Upload stages rows into an ImportBatch with status pending. Nothing reaches the ledger before user confirmation. |
| BR-005-10 | The preview summarises: rows read, new rows, duplicates skipped, rows needing attention, and the resulting date range. |
| BR-005-11 | The user can drill into any category and inspect individual rows before committing. |
| BR-005-12 | Cancelling writes nothing to the ledger and deletes the staged rows and the source file. |
| BR-005-13 | Commit is atomic: the batch applies fully or not at all. |
| ID | Rule |
|---|---|
| BR-005-14 | Each row receives a deterministic natural key: date + normalized product + movement type + quantity + value + institution + user. The institution component is the institution the extract's name resolves to, not the text B3 wrote: an explicit, reviewable alias table names the spellings of one institution, and case, accents, punctuation and whitespace runs are ignored when matching. A spelling the table does not name resolves to its own institution rather than being merged by similarity, and an alias added later needs a data migration to move the rows already keyed on the spelling it merges. (Amended 2026-09-20, #136 — see DL-005-16.) The asset component is likewise the asset the extract's code resolves to: a second explicit, reviewable alias table names the codes B3 writes for one instrument — an auction settlement ticker beside the listed ticker it settles — with only case and surrounding whitespace ignored. No suffix or similarity rule is ever inferred from a code's shape, a code the table does not name becomes its own asset, and an alias added later needs a data migration on the same terms. The fractional-market F suffix stays outside this table: it names an order book, is mechanical and holds for every ticker. (Added 2026-09-20, #135 — see DL-005-17.) A Tesouro Direto Produto is the one code read by its words: it resolves through a hand-listed product table — one indexer word, the coupon words, the product's own name words, with case, accents and whitespace ignored, and any other word meaning no match — to the catalogue code the Tesouro price sync writes, whichever way B3 spelled the product. (Added 2026-09-27, #164 — see DL-005-24.)
|
| BR-005-15 | A row whose natural key already exists is classified as duplicate and skipped, not inserted. Enforced by a unique constraint (DM-1), not by application logic alone. |
| BR-005-16 | Genuine same-day identical transactions are not duplicates. Two identical buys of the same ticker at the same price on the same day are real. An occurrence counter within the natural key distinguishes the second legitimate occurrence from a re-imported row. |
| BR-005-17 | Re-importing an identical file produces zero new ledger rows and reports as such — including after the mapping table changed, a row was classified by hand, or a hand-classified row was later re-typed in the ledger: an occurrence stored under any other key the same B3 row can have (mapped, unmapped, or staged for want of a price) counts as that row's. Once stored under either seven-segment import key, the key remains the B3 source row's identity while its asset, institution, date and quantity are unchanged; its internal ledger type, price and fees are interpretations the user may correct and do not re-key it. (Amended 2026-09-14, #110; 2026-09-29, #178.) |
| ID | Rule |
|---|---|
| BR-005-18 | B3 Movimentação types map to internal transaction types through an explicit, versioned mapping table. Version 3 maps APLICAÇÃO → buy, an ordinary fixed-income/FII Resgate and RESGATE ANTECIPADO/ → sell, and Restituição de Capital → amortisation. (Amended 2026-09-14, #110.) Version 4 also names the corporate-event rows — Desdobro (credit, states the shares added), Grupamento (credit, states the resulting quantity), Fração em Ativos (debit, no price) and Leilão de Fração (credit, price and value) — which stage exactly as version 3 stored them (unmapped key form) and are resolved at commit by BR-005-20b. (Amended 2026-09-15, #113.) Version 5 also names asset-conversion evidence — Atualização, Incorporação, a price-less Resgate on a listed asset, and price-less Transferência legs whose asset codes together match an explicit cross-asset definition — without changing ordinary custody-transfer mapping; commit resolves only an explicitly defined complete source/target conversion under BR-005-20c, while every other Resgate keeps version 3's sell meaning and every other Transferência keeps its transfer meaning. The one exception is a priced Resgate on a source of a liquidation definition (BR-005-20c). It is still staged as version 3's sell, and commit reprices it in place, key kept, as a sale at the fund's liquidation value. This includes a copy an earlier import already applied as an active sell, unless a user edited it. (Amended 2026-09-19, #121; 2026-09-21, #143.) Version 6 also names the subscription paperwork — Direito de Subscrição, Solicitação de Subscrição, Direitos de Subscrição - Não Exercido, Direito Sobras de Subscrição - Não Exercido, Cessão de Direitos, Cessão de Direitos - Solicitada and Recibo de Subscrição — which stage ignored (BR-005-19); Direitos de Subscrição - Exercido keeps its price-less subscription mapping as evidence for BR-005-20d. (Amended 2026-09-21, #144.) Version 7 also maps Juros Sobre Capital Próprio - Reativado → jcp. For dividend, JCP, rendimento and amortisation cash rows only, a missing unit price may be recovered from the stated operation total divided by a positive quantity, only when the result is non-negative, exactly representable in NUMERIC(20,8) and reproduces that total exactly; an explicitly stated unit price takes precedence. Such a recovered price is a known price, not an estimate. Its natural key retains the original missing-price/raw-movement identity and occurrence sequence, so earlier zero-placeholder rows are activated in place and different cash totals do not split their historical occurrence group. A directly stated zero price remains a distinct source identity: historical aliases count only transactions whose original import row actually stated its unit price. (Amended 2026-10-01, #158.) Version 8 also names Cobrança de Taxa Semestral as ignored (BR-005-19): the B3 cash custody fee moves no security quantity or cost basis and is not income. (Amended 2026-10-02, #159.)
|
| BR-005-19 | An unrecognised movement type is never silently dropped. It is imported with status unclassified, excluded from all calculations, and surfaced in the "Needs attention" queue. A row that mirrors a record another extract owns (Transferência - Liquidação, Dividendo - Transferido, Juros Sobre Capital Próprio - Transferido) is instead classified ignored: stored and visible on the batch, never written to the ledger, not in "Needs attention", and still classifiable by hand under BR-005-20. Re-importing a file whose mirror rows an earlier map version stored as unclassified transactions marks each untouched one superseded and its original row ignored. (Amended 2026-09-15, #110 — see DL-005-09.) A Leilão de Fração whose price is carried into a split, grupamento, conversion or liquidation fraction sale (BR-005-20b) is treated the same way: its stored copy superseded, its row ignored. (Amended 2026-09-15, #113.) A row recording subscription rights that move no position (BR-005-18 v6) is ignored the same way, stored copies included; so is an Atualização resolved as a position refresh (BR-005-20e). (Amended 2026-09-21, #144.) A Direitos de Subscrição - Exercido row consumed by BR-005-20d remains visible as ignored, but cannot be classified by hand: its shares are already carried by the main-asset credit. (Amended 2026-09-30, #179 — see DL-005-28.) A Cobrança de Taxa Semestral cash custody-fee row is also ignored, stored untouched unclassified copies included; it remains visible without changing positions, cost basis or earnings. (Amended 2026-10-02, #159.)
|
| BR-005-20 | The user can manually classify an unclassified or ignored row, bringing it into calculations, except an ignored subscription exercise already consumed under BR-005-20d; both the batch form and the classification use case refuse that exercise. The main-asset credit remains editable under SPEC-006. A row B3 exported without a price cannot be classified as a type whose effect depends on price. Re-importing a file whose rows an earlier map version stored as unclassified classifies, in place, each one the current map now maps and no user has touched — key kept, not marked as a user edit, recalculated; a row whose position cannot replay with it stays unclassified. (Amended 2026-09-15, #110.)
|
| BR-005-20a | A Transferência credit exported without a price takes, at commit, the average cost of the source position immediately before the matching debit is applied in replay order — the source position including the ledger and the batch's own rows. The matching debit is in the same batch, on the same asset, date and quantity, at a known institution — which may be the credit's own — and each is the other's only match. A same-institution pair moves nothing: the source position is the destination, so the credit carries the average the debit removed the shares at, the quantity returns exactly and the cost to the ledger's storage scale. A debit is never written while a price-less credit on its own position, date and quantity is not active in the ledger — asked of that relation rather than of a formed pair, because the shapes where no pair forms (two same-position pairs of one quantity, a same-institution debit competing with a cross-institution one) are precisely the ones where two debits could empty a position between them. The debit is refused rather than applied, nothing is written for it and no occurrence is taken, so importing the file again applies both once the credit can take its cost; it is refused before any other row of the batch is given up, so a sale on the same position does not lose its place to a debit that was never going to be written. A cross-institution debit is still applied on its own, its credit waiting in "Needs attention" for history not yet imported. With no such match, a debit that is not written, or a source that cannot be replayed, held fewer shares or held them at no cost, the row stays unclassified. The carried figure is written at that storage scale, because a carried cost is a division and repeats as often as not: a full-precision figure would make the position a commit caches disagree with the position a rebuild folds from the same rows read back (DM-4). A later import of the same file recomputes that transfer's cost unless a user has edited it — an unclassified one takes it, a carried one whose figure changed is updated in place — keeping its key and without marking it as a user edit; a cost that can no longer be computed is kept. (Amended 2026-09-14, #110 — see DL-005-10; amended 2026-09-20, #135 — see DL-005-17.) A same-position round trip pairs (#145): where as many price-less credits as debits share one asset, institution, date and quantity and match only each other, every pairing is the same no-op, so they pair in any order and each credit carries the position's average from before the round trip; unequal counts, a credit at another institution or with a price of its own, or a competing debit at another institution, still pair nothing, and a round trip one of whose debits is not written carries nothing. (Amended 2026-09-21, #145 — see DL-005-20.)
|
| BR-005-20b |
Corporate-event rows are resolved at commit, in replay order, after transfer carries (BR-005-20a) and before positions replay; a row a user has classified by hand is never touched. Desdobro/Grupamento apply SPEC-007 BR-007-04a: the ratio derived from the position immediately before the event and B3's stated quantity must agree with B3's published factor (SPEC-008 BR-008-29) dated within a configured window before the row; otherwise — no basis, no or ambiguous factor, disagreement, a ratio not exactly storable, or two ratio events on one position and date — the row stays unclassified and the batch page shows the derived and published figures. A Desdobro and a Grupamento on one position and date, the only two ratio events there, are decided as one ordered pair: each against its own published factor, in the one order — and, for the second, the one basis, the first's whole result or its floor where B3 removed the first's fraction in between — that reproduces both stated quantities exactly. Both apply or neither does, with one reason: any disagreement refuses the pair (disagrees), never one of the two; an order the quantities do not single out, more than two ratio events, two of one kind, one already settled, or an issuer with no published factor at all refuses combined_same_day. A fraction removed between the pair takes the first event as its origin and, because replay applies both events before any sale that day (SPEC-007 BR-007-15), is sold at the second event's scale — quantity × its multiplier, the auction's unit price ÷ it, the same proceeds — and refuses scale_not_representable where either figure is not exact at eight places. (Amended 2026-09-21, #139 — see DL-005-18.) Fração em Ativos takes its origin from the unique share-base event on the same asset and institution, within a configured origin window (default 60 calendar days), whose resulting fractional quantity equals the fraction: after a bonificação it becomes fração de bonificação (SPEC-007 BR-007-05a) and its paired Leilão de Fração a leilão de frações provento; after a split or grupamento it becomes a sale dated at the fraction's date and priced by its paired Leilão de Fração (BR-007-04b). A fraction and an auction pair one-to-one on asset, institution and quantity, the auction on or after the fraction within a configured window, each the other's only match, within the batch and the active ledger. A fraction on a position whose shares arrived through a BR-005-20c group may also take an origin from that group's source, in addition to any share-base event the position holds of its own: the group counts as one arrival, so the fraction it left is the one immediately after all of its incoming legs on the position, and its type is that of the unique share-base event on the source position whose resulting fractional quantity equals the group's outgoing quantity, within the same origin window measured from that event's date. The traced candidate competes with the position's own events rather than replacing them, so a position holding both, each leaving the same fraction, is ambiguous_origin. Where no trail exists — the outgoing quantity is no event's fractional part — the conversion itself is the fraction's origin when every outgoing quantity of the group is whole, its incoming total on the position is not, and the group lies within the same origin window. A liquidation's acquisitions (BR-005-20c) are an origin in the same way, when their total on the position is fractional and within the window. The fraction is then read like a split's: a sale dated at the fraction, priced by its paired Leilão de Fração, with the auction consumed. (Amended 2026-09-21, #143 — see DL-005-21.) Otherwise the fraction refuses no_origin; where a trail exists but cannot be read — two matching source events, a ratio event on the source still unresolved anywhere in the batch or the ledger, or several outgoing legs disagreeing — it refuses origin_unresolved, because unknowable and absent call for different action by the person reading the queue. With no unique origin or pair the rows stay unclassified. Resolved rows stored unclassified by an earlier import are activated in place, key kept, not marked as a user edit (BR-005-20). The origin is taken after the position has been replayed with the conversions of BR-005-20c that precede it, and the conversions are then measured again against the settled events. Where B3 publishes no factor of any kind for the issuer — its factor list is empty outright, not merely empty within the window, as it is for a fund and for a delisted company — the row may instead be confirmed by corroboration across positions: the same asset, date and movement on two or more positions, every one with a replayable positive basis, every one deriving the same ratio, that ratio exactly storable. That derived ratio is then applied. A published factor always takes precedence and corroboration is never reached where one exists; one position never confirms itself (no_factor); any disagreement refuses the whole set (disagrees), never a majority; a ratio that cannot be stored exactly refuses not_representable. The count of two is structural, not configurable. (Added 2026-09-15, amended 2026-09-18, #113, 2026-09-19, #128, 2026-09-19, #129, and 2026-09-19, #120 — see DL-005-10, DL-005-12, DL-005-13, DL-005-15.)
|
| BR-005-20c |
Asset conversions are resolved as one atomic group at commit, after transfer carries, and measured against a position whose BR-005-20b corporate events have settled: conversions are planned once, corporate events are settled once against those legs, and conversions are then planned again against the settled events. The dependency runs both ways and the passes are bounded, never iterated to a fixpoint. The versioned conversion table names every allowed source and target asset code; an Atualização is only balance evidence and never sufficient by itself. Where a definition opts in (sourceBalanceRestatements), a source Atualização stating exactly the balance the replay already holds is corroboration and is set aside, not read as what remains. It stays unclassified. For every other definition such a statement still means nothing converted, and the group refuses. Resolution replays every involved position, derives each statement's delta, and emits linked conversion_out/conversion_in legs only when the definition and evidence identify one complete, unique group within the configured conversion window. The outgoing legs remove quantity at average cost, the incoming legs receive exactly the same total cost, and no realised gain or cash value is recorded (SPEC-007 BR-007-05b). A conversion never carries cash. A reorganisation in which B3 states cash, as in BPFF11/HGFF11, is a disposal and not a conversion. (Amended 2026-09-21, #143 — a cash-bearing conversion added and removed the same day, see DL-005-21.) A fund liquidated into another fund's shares is not a conversion but a sale and an acquisition, and needs an explicit liquidation definition. The definition names each source with its public liquidation value and the target with its public unit cost, as the fund administrator publishes both, plus the date the source's trading was blocked. Each source's priced Resgate is repriced in place as a sell at the liquidation value, fees zero, realising its result through the ordinary sale path. The target's credits (B3 may state them on a receipt code) become, in place, subscriptions at the unit cost on the target's ledger code. The source Atualização restatements stay unclassified. It is all or nothing: the liquidation applies only when every source's redemption equals its whole position and every target credit is present within the conversion window. The rows may be in this batch or stored by an earlier import; a stored active sale on a source dated on or after its blocked date is that source's redemption, because no ordinary trade can occur there. Nothing is written from a partial extract, and the liquidation applies whichever file carrying its rows is imported last. Rows that depend on it and were staged by an earlier file, such as its fraction or a later rename, settle on the next import that carries them. A re-import changes nothing, and a row a user edited is never touched. Liquidations are planned before conversions. (Amended 2026-09-21, #143 — see DL-005-21.) One target receives all carried cost; multiple targets require explicit exact allocation weights in the definition, with any storage-scale residual assigned deterministically to its last positive-weight target and repeated same-target evidence sharing that target allocation proportionally, with its residual on the final row. Repeated target evidence is permitted only for same-day Transferência credits; two statements of one code otherwise refuse as ambiguous. A weight of zero is permitted and means this target carries no cost — B3 put shares on it and attributed no value to them, the same reading SPEC-007 BR-007-05 already takes of a quantity B3 states without a price; at least one weight must be positive, and a negative or missing one still refuses. Without source history, sufficient quantity, a unique complete group, or multi-target weights, every row stays unclassified. Rows stored unclassified by an earlier import activate in place where a row-backed leg exists; generated companion legs use deterministic keys; neither is marked as a user edit, and a second re-import changes nothing (BR-005-17/20). A group already written keeps its stored legs, so adding a target to a definition strands any group written under the older version: its remaining evidence can never complete the wider group, and the only remedy is to delete that group and import again — a required step of such a change, not an optional one. The v1 definitions include the observed KLBN11 unit decomposition into KLBN3/KLBN4 at explicit 1:4 weights; its price-less transfer rows are consumed as conversion legs only when the complete group is present. v5 adds bidi11-to-inbr32, Banco Inter's 2022 migration to Nasdaq, as a single one-to-one definition: B3's record walks BIDI11 → INHF12 → INBR32 but states the same quantity at every step and prices none of them, and the two statements lie 70 days apart, beyond any one group's window. The intermediates are deliberately left undefined rather than modelled as cost-bearing steps B3 never priced. v6 adds four one-to-one ticker renames that B3 records only as an Atualização credit on the new code: wizs3-to-wizc3, trpl4-to-isae4, odpv3-to-saud3 and mall11-to-pmll11. v7 added bpff11-and-hgff11-to-rvbi11, a cash-bearing conversion of two funds into RVBI11 (a capability since removed), and rvbi11-to-psec11, the rename that follows it. v8 removes that conversion before any ledger wrote a group under it. In its place is the liquidation definition bpff11-and-hgff11-liquidated-into-rvbi11: BPFF11 at 62,03538245 and HGFF11 at 71,04670108, both blocked from trading on 2025-08-18, into RVBI11 through RVBI15 receipts at 64,15. rvbi11-to-psec11 is kept. (Amended 2026-09-21, #143 — see DL-005-21.) (Amended 2026-09-19, #121, #128 and #129 — see DL-005-11, DL-005-12, DL-005-14.)
|
| BR-005-20d |
An exercised subscription resolves at commit as one subscription on the main asset. A Direitos de Subscrição - Exercido debit and a price-less Atualização credit pair when they share the issuer root and institution, carry the same quantity, the credit falls on or after the exercise within the configured import.subscription_credit_window_days, and each is the other's only match. No pair forms when an asset-conversion or liquidation definition names the credit's code or claims the row, when the exercise was classified by hand or is already active, or when the position's replayed balance just before the credit equals the credited quantity (the Atualização may be a balance statement). B3 states no subscription price, so the subscription is dated the credit date and priced at the main asset's stored close on that date, or the nearest earlier stored close however old, and is marked as an estimate (SPEC-007 BR-007-06). With no stored close both rows stay unclassified and a later import resolves them. The exercise is superseded and its row ignored. An applied subscription is never repriced by a later import; a user edit of its price clears the marker. (Added 2026-09-21, #144 — see DL-005-22.) A credit a user already classified by hand is never changed by import, but still decides its exercise. When the pair otherwise forms, and the credit's type adds shares (buy, subscription, bonificação, transfer in, adjustment), there are two cases. If the credit carries a cost (a unit price above zero), the exercise is superseded at commit and its row ignored, because the user has already answered the cost question. If it carries zero cost, the exercise stays in "Needs attention" with two actions. Resolve as subscription re-types the credit in place: it becomes a subscription priced at the main asset's stored close on or before its date, marked as an estimate, with its key kept and marked as a user edit. The exercise is superseded, its row ignored, and carried legs downstream are re-derived (SPEC-007 BR-007-06). Keep my classification leaves the credit unchanged, supersedes the exercise and ignores its row. Neither action is ever applied without the user. The close is fetched ahead of commit for such a pair as for any other. Without a stored close, Resolve as subscription is refused with that reason, and a later import fetches it. The user is never asked for a price. (Amended 2026-09-28, #157 — see DL-005-25.) Once any of these paths supersedes the exercise and marks its row ignored, the exercise has no separate classification action: its quantity is already represented by the credit, so reactivating it would count the shares twice. (Amended 2026-09-30, #179 — see DL-005-28.)
|
| BR-005-20e |
An Atualização credit that restates the whole position is a position refresh, not a movement: where its quantity equals the position's replayed balance at its date (ledger and batch together), the balance is above zero, no subscription evidence on that issuer falls within the window, and no other resolver (BR-005-20b–20d) claims it, its stored copy is superseded and its row ignored. (Added 2026-09-21, #144 — see DL-005-23.)
|
| BR-005-20f |
The user can mark an unclassified row ignored, with a stated reason, from the row itself. Nothing reaches the ledger. The reason is stored with the row. The row leaves "Precisa da sua atenção", and BR-005-20 can bring it back into calculations later. (Added 2026-10-09, SPEC-022 — see DL-005-31.)
|
| BR-005-21 | Unmapped type strings are logged (the type string only, no values) so the mapping table can be extended. |
| ID | Rule |
|---|---|
| BR-005-22 | Committing a batch containing a Posição file triggers comparison of computed positions against the B3 snapshot as of that date. The export states no reference date, so the user confirms it on the preview (defaulting to today, never in the future) and a Posição batch does not commit without it. The prompt states that Posição is the settled custody at the time of export: the date to enter is the export date, never the upload, commit or current date. The comparison covers every Posição row read and every open ledger position the file is authoritative for — any asset class the file has at least one read row of, at any institution the file names on any row — and a ledger position the file does not list compares against a B3 quantity of zero. A position first traded after the reference date is not one the file could list, and is left out. (Amended 2026-09-14, #108 — see DL-005-08; 2026-09-21, #145 — see DL-005-19; 2026-09-29, #146 — see DL-005-26.) |
| BR-005-23 | The report lists per asset: computed quantity, B3 quantity, difference. |
| BR-005-24 | Discrepancies are explained by likely cause where determinable: missing history before the import range, buys or sells after the snapshot date or not yet settled at B3's D+2, unclassified rows affecting that asset, an uncaptured corporate event. The trade cause is positive evidence: removing exactly those active buys and sells must reproduce B3's quantity, with D+2 counted on B3 trading days; proximity alone is not enough. "Unclassified rows affecting that asset" reads the ledger's unclassified transactions on the position, not the Posição batch's own rows, which never hold any. A position the ledger holds and the snapshot does not list is attributed to its absence from the snapshot — the trace a ticker change, merger or delisting the ledger never recorded leaves — unless unclassified rows on it explain it first. A quantity deficit by itself is not missing history: that cause applies only when there is no active history, or the greatest quantity the recorded position ever reached is still below B3's figure. (Amended 2026-09-15, #113; 2026-09-21, #145; 2026-09-29, #146 — see DL-005-26.)
|
| BR-005-25 | Accepting the B3 figure creates a dated, clearly-labelled adjustment transaction. History is never silently overwritten. It is refused, writing nothing, when the ledger holds no active transaction for the position at or before the reconciliation date — the history has not been imported — or when the ledger's quantity is no longer the one the report computed; both are checked against the ledger at the moment of accepting, and where either applies the report offers no accept action and says what to import. It is also refused for a position the snapshot does not list: zeroing it would post away the cost a ticker change or merger still has to carry to the new asset, so the history is the correction. (Amended 2026-09-15, #110; 2026-09-21, #145.) |
| BR-005-26 | Portfolio reconciliation status (reconciled / discrepancies found / never reconciled) is visible on the dashboard. |
| BR-005-26a |
The dashboard measures open discrepancies against the current ledger, not the ledger at commit. • Each unresolved discrepancy of the most recent Posição is compared again with the position as replayed at that Posição's date. • A discrepancy that now agrees is not counted. • The batch's stored report is never rewritten; it records what the import found. • The reconciliation screen shows both the stored figure and the current figure. (Added 2026-10-09 — see DL-005-32.) |
| ID | Rule |
|---|---|
| BR-005-27 | Every screen showing portfolio value displays the valuation as-of date and the date of the most recent custody import. |
| BR-005-28 | When the last import exceeds the configured staleness threshold (default 30 days), a non-blocking refresh prompt appears with a direct link and the steps. |
| BR-005-29 | Optional monthly email reminder, opt-in, per SPEC-004 consent rules. |
-
An extract can be uploaded on its own or alongside the others, in any order, and reaches the same staged preview. (The guided first-run flow itself moved to SPEC-020 at M8, which also replaced FR-2.1’s screenshots with project-authored diagrams — see SPEC-020 DL-020-06.)
-
Files upload together or individually, in any order.
-
A renamed file is still correctly identified by structure.
-
A file with leading metadata rows and reordered columns parses correctly.
-
An unparseable file yields an error naming the expected structure, not a generic failure.
-
Indexer, issue date and maturity are extracted from the Posição
Renda Fixatab intoFixedIncomeContract, and the contracted rate the user types is kept across later Posição imports. (Amended 2026-09-14, #108.) -
No CPF exists anywhere after importing a real extract (joint with SPEC-004).
-
The preview shows accurate counts and no ledger row exists until commit.
-
Cancelling leaves the ledger unchanged and removes staged rows and the source file.
-
A commit interrupted mid-way leaves no partial batch.
-
Importing full history, then re-importing an overlapping range, yields identical positions and zero duplicates.
-
A row stored under a seven-segment import key, classified by hand and then re-typed with a price keeps that source key; re-importing the file adds zero rows and does not count its quantity twice. (Added 2026-09-29, #178.)
-
Two genuine identical same-day trades both import; re-importing that file adds neither again.
-
An unknown movement type imports as
unclassified, is excluded from calculations, and appears in "Needs attention". -
A settlement mirror (
Transferência - Liquidação) imports asignored, writes no ledger row, stays out of "Needs attention", is listed on its batch, and can be classified by hand. (Added 2026-09-14, #110.) -
Classifying an unclassified row brings it into calculations and triggers recalculation.
-
Re-importing a file whose Desdobro, Grupamento, Fração em Ativos and Leilão de Fração rows are stored
unclassifiedresolves each one BR-005-20b can resolve, in place and key kept; a second re-import changes nothing; an unresolved one staysunclassifiedwith the derived and published figures shown. -
A
Fração em Ativoson a position whose shares arrived through a conversion resolves against the share-base event behind the group's outgoing leg, and staysunclassifiedwhere that trail is not exact. (Added 2026-09-19, #129.) -
Where no source trail exists and the group's whole outgoing quantities produce a fractional incoming total, the
Fração em Ativostakes the conversion as its origin and is sold at its auction's price, the auction consumed. (Added 2026-09-21, #143.) -
A liquidation definition sells each source at its liquidation value, realising its result, and acquires the target at its unit cost. It applies whichever order the files are imported in and wherever an extract ends, and nothing is written until every source redemption and target credit is present. (Added 2026-09-21, #143.)
-
A fraction left by a liquidation's acquisitions is sold at its auction's price against the target's average cost. (Added 2026-09-21, #143.)
-
A price-less
Transferênciadebit and credit at one institution returns the position's quantity exactly and its cost to the ledger's storage scale, and a later sale of those shares applies. (Added 2026-09-20, #135.) -
Where such a pair's credit is not written
active— it took no carried cost, or the legs are too ambiguous to pair at all — the debit is refused rather than applied, the position and every other row on it are unchanged, and importing the file again once the history is in applies both legs. (Added 2026-09-20, #135.) -
A Negociação disposal printed under B3's auction settlement ticker applies against the listed asset it settles, leaving one asset in the catalogue. (Added 2026-09-20, #135.)
-
Re-importing a file whose asset-conversion evidence is stored
unclassifiedresolves each complete BR-005-20c group atomically, keeps row-backed keys, creates deterministic companion legs once, and preserves total cost with no realised gain; incomplete, ambiguous or unallocated groups stayunclassified. -
An exercised subscription and its later
Atualizaçãocredit resolve as onesubscriptionon the main asset at the credit date, priced at the stored close on or before that date, marked as an estimate; without a stored close both rows stay in "Needs attention" and a later import resolves them; a second import changes nothing. (Added 2026-09-21, #144.) -
Subscription paperwork rows import as
ignoredand leave "Needs attention", storedunclassifiedcopies included. (Added 2026-09-21, #144.) -
A zero-quantity
Cobrança de Taxa Semestralcash debit imports asignored, stays visible on its batch and writes no ledger row or change to positions, cost basis or earnings. Re-importing supersedes each untouched historicalunclassifiedcopy and marks its origin rowignored; a further import changes nothing and user-edited copies remain untouched. (Added 2026-10-02, #159.) -
An
Atualizaçãoequal to the position's balance on its date isignoredand moves nothing; one that pairs with a subscription exercise is never taken for a refresh. (Added 2026-09-21, #144.) -
An exercise whose credit the user classified by hand leaves "Needs attention" at commit when that classification carries a cost. When it carries zero cost, the exercise offers Resolve as subscription, which turns the credit into an estimated
subscriptionat the stored close and recalculates the position, and Keep my classification, which leaves the credit as it is. Both clear the exercise, and a later import changes nothing. (Added 2026-09-28, #157.) -
A resolved
Direitos de Subscrição - Exercidorow remains visible in Ignored without a classify form, and a direct or stale classification request is refused without changing ledger rows or position quantity; other ignored rows remain classifiable. (Added 2026-09-30, #179.) -
A discrepancy on a position holding
unclassifiedledger rows reads "unclassified rows affecting that asset", not missing history. -
A refused row whose position is short names its likely cause — an uncaptured corporate event, unclassified rows, or history missing from before the import range — and the missing-history case directs the user to a manual opening position rather than to another import.
-
A
DesdobroorGrupamentoon an issuer B3 publishes no factor for resolves when two or more positions derive the same storable ratio, and refuses when only one does, when they disagree, or when the issuer does have published factors. -
A same-date
DesdobroandGrupamentoon one position resolve together in the order their stated quantities prove, each at its own published factor; the fraction removed between them sells at its auction's proceeds and the position reconciles to B3's count. Where the quantities fit no order, more than one, or either factor is missing or not storable, both stayunclassifiedwith the same reason. (Added 2026-09-21, #139.) -
Reconciliation against a same-date Posição snapshot reports zero discrepancies for a correctly imported portfolio.
-
An induced discrepancy is reported per asset with a plausible cause.
-
A Friday sale settling on Tuesday against a Posição exported on Monday is attributed to a trade not yet settled at D+2; a trade after the confirmed date is attributed to the same cause only when removing those trades reproduces B3's quantity exactly. A fully acquired and later sold position is never called missing history merely because the ledger is below B3, while history that never reached B3's quantity still is. (Added 2026-09-29, #146.)
-
Accepting a B3 figure creates a visible adjustment transaction, not an edit to history.
-
An import of 10,000 rows previews in under 30s and commits in under 60s.
-
Import staleness prompt appears after the configured threshold and the threshold is config-driven.
-
A reactivated JCP payment maps to
jcp; a cash payment with no unit price but an exactly representable stated total imports at that exact amount. Re-import activates untouched historical zero-placeholder rows in place, preserving keys and all occurrences, while directly stated zero-price payments remain separate in either import order. (Added 2026-10-01, #158.) -
An
unclassifiedrow markedignoredwith a reason leaves "Precisa da sua atenção", writes nothing to the ledger, keeps its reason, and can be brought back under BR-005-20. (Added 2026-10-09, SPEC-022.) -
After a ledger re-derivation that corrects a position, the dashboard no longer counts that position's discrepancy as open. A trade dated after the Posição never creates one. The batch's stored report is unchanged. (Added 2026-10-09.)
- Automatic sync from B3 by API or credential-based scraping (PRD §4.1 — unavailable and rejected respectively).
- The Open Finance aggregator adapter — the port is built, the adapter is v2.
- Broker-specific note (nota de corretagem) parsing.
- Investment fund (fundos) positions.
- OFX, CSV or any non-B3 file format.
- Manual transaction entry (SPEC-006).
- Tax basis or IR reporting derived from imports.
- Editing history in place to resolve discrepancies — explicitly replaced by adjustment transactions.
| # | Decision | Alternatives considered | Rationale |
|---|---|---|---|
| DL-005-01 | File import as the v1 ingestion mechanism. | B3 API direct; Open Finance aggregator; credential scraping | B3 APIs are B2B-only and unavailable to individuals. Aggregators cost ~R$2.5–6k/month and need a CNPJ. Scraping needs stored B3 credentials — rejected in SPEC-003 DL-003-05. File import is free, legal and robust; its cost is manual friction, mitigated by staleness UX. |
| DL-005-02 | Staged preview before commit, always. | Import directly with an undo | Undo of a partially-applied financial ledger is far harder than preview-then-commit, and the user's trust is established or destroyed on the first import. Seeing what will happen beforehand is worth the extra step. |
| DL-005-03 | Structure-based file identification, not filename. | Require specific filenames; ask the user which file is which | Users rename downloads. Asking adds a step and users guess wrong. Structure is unambiguous and costs nothing. |
| DL-005-04 | Natural-key dedupe with an occurrence counter. | Hash the whole row; trust B3 transaction IDs; ask the user to avoid overlaps | B3 extracts carry no stable per-row identifier. A plain hash would wrongly collapse two genuine identical same-day trades into one — a real and silent data-loss bug. The occurrence counter distinguishes "same row imported twice" from "two real identical trades". |
| DL-005-05 | Unmapped types imported as unclassified rather than dropped or guessed. |
Drop them; map to a best-guess type | Dropping loses data silently and breaks reconciliation with no explanation. Guessing corrupts calculations invisibly. Surfacing keeps the ledger honest and turns an unknown into a visible, resolvable item — and feeds the mapping table. |
| DL-005-06 | Discrepancies resolved by adjustment transaction, never by editing history. | Overwrite computed positions with B3's figures | Overwriting destroys the audit trail and violates the PRD's "every number is explainable" principle. An adjustment row is visible, dated, reversible, and explains itself in the transaction list. |
| DL-005-07 | Source file deleted after commit or cancel. | Retain for support and re-processing | Retention would ease debugging, but the file is the only place a raw CPF exists (SPEC-004 DL-004-04). Parsed rows carry everything needed. |
| DL-005-08 | Posição's contracted rate is typed by the user, and its reference date is confirmed by the user. (2026-09-14, #108.) | Parse the rate (impossible — no such column); read the date from the filename; use the upload date | The first real Posição export has no Categoria, no reference date and no contracted rate on any tab. The rules had been written against an invented layout. Reading the filename contradicts DL-005-03. The upload date silently mis-reconciles an export that is older than the upload. A date the user confirms costs one field, and the user already knows the rate from the broker statement. |
| DL-005-09 |
Rows mirroring another extract's record are ignored, not unclassified. (2026-09-14, #110.)
|
Leave them unclassified; drop them; map settlements to trades |
The first real Movimentação had 684 settlement and broker-transfer mirrors among 1,214 unclassified rows. In Needs attention they bury the rows that do need a decision, and classifying them as trades doubles what Negociação already imports (BR-005-01). Dropping them breaks BR-005-19. ignored keeps them stored and visible and classifiable by a Movimentação-only user. |
| DL-005-10 | Corporate-event rows resolve at commit and stage unchanged; the configured fraction-origin window defaults to 60 calendar days. (2026-09-15; amended 2026-09-18, #113.) | Map them at staging; ask the user to classify each; keep the original 30-day default | Every such row on a real ledger is already stored unclassified under the unmapped key; mapping at staging would change keys and import them a second time (BR-005-17). Resolution needs the replayed position before the event, which only commit settlement has — the same reason BR-005-20a's carry lives there. Hand classification stays the fallback for what cannot be confirmed. The first personal reconciliation found valid bonus-to-fraction intervals of 35, 37 and 38 days; 30 days refused them, and 45 was set to cover that maximum. A 48-day interval then appeared (AXIA7, #128), so the bound is 60 calendar days. Widening it can only ever turn a resolution into an ambiguous_origin refusal, never into a wrong attribution — the exact fractional-part equality and the uniqueness guard are what establish correctness — so the margin is deliberately generous rather than tracking each observed maximum. (Amended 2026-09-19, #128.)
|
| DL-005-10 | A price-less custody transfer carries the source's average cost immediately before the debit, resolved at commit and recomputed on every re-import. (2026-09-14, #110.) | Ask the user for a price; commit at zero; use the day before, at staging; freeze the figure at first commit | The first real Movimentação left 115 transfers without a price. Zero opens a lot at no cost; a typed price guesses a number the ledger knows. Staging saw only the ledger, so a bonificação or buy earlier in the same file was missed. A figure frozen at first commit let import order decide it — a transfer imported before a backdated source buy kept the lower cost. Re-importing the transfer file promotes or corrects every unedited carried transfer in place, and all of a commit's corrections are validated and recalculated together. Not covered: a source edited or deleted later leaves the carried cost stale until the transfer file is imported again. |
| DL-005-11 |
Asset conversions use explicit versioned definitions and grouped ledger legs; Atualização is evidence, not a transaction by itself. (2026-09-18, #121.)
|
Reuse transfer pairs; map every Atualização to an adjustment; ignore the rows |
A real Atualização states only an absolute balance for the current code — no old code, ratio or cost — and repeats for ordinary balance changes, so treating it as an acquisition guesses both provenance and cost. The same ledger contains one-to-one, chained and one-to-many code conversions. An explicit definition makes the relationship and any multi-target allocation reviewable; grouped legs preserve auditability, exact cost and idempotency without inventing a sale. |
| DL-005-12 | Conversions are measured against a position whose corporate events have settled, in two bounded passes. (2026-09-19, #128.) | Keep conversions strictly before corporate events; swap the order; iterate both to a fixpoint | An unresolved Fração em Ativos is stored unclassified and excluded from replay, so planning conversions first measured AXIA7 as 68,34 rather than 68 and moved 4,34 units — and 4,34/68,34 of the cost — into AXIA13 instead of 4: a quantity error and a silent cost-basis error, each indistinguishable from a correct figure on the report. The order cannot simply be swapped, because the dependency is mutual: the KLBN3/KLBN4 fractions need their KLBN11 conversion legs in the replay before their origin can be computed. Two bounded passes — conversions, one corporate settlement, conversions again — resolve both directions and stay deterministic, where a fixpoint loop would reintroduce what #125 removed. A conversion group already written keeps its stored legs: re-resolution is gated before the key, so a definition or window change is remediated by deleting the group and re-importing, not silently in place. |
| DL-005-13 | A fraction on a conversion target takes its origin from the group's source, and only where the trail is exact. (2026-09-19, #129.) | Treat conversion_in as an origin type in its own right; treat such a fraction as a plain sale; leave it unclassified
|
B3's bonificação left KLBN11 holding a fractional unit, which B3 then decomposed into its component shares and auctioned off the targets. KLBN3 and KLBN4 own no share-base event, so every candidate list was empty and both fractions refused no_origin for good — KLBN4 reconciled at 2,4 against B3's 2, and KLBN3, absent from B3's Posição, silently kept 0,6 phantom shares. Making conversion_in an origin type of its own matches the same fractional parts but has to decide the fraction's tax treatment — exempt income or a realised disposal — without knowing what caused the conversion; tracing through the group gets it for free and conserves the cost the units carried across (BR-007-05b). The trail is exact rather than inferred: the group's outgoing quantity must itself be precisely the fractional part a unique share-base event left on the source, which is the same arithmetic run one asset upstream. A group is atomic, so its incoming legs are read together: taken singly, two same-day credits' replay order is a UUID tiebreak and one ordering makes the origin ambiguous by coin flip. Not covered: a conversion of a whole position, whose outgoing quantity is no event's fraction, traces to nothing and stays unclassified — deliberately, since guessing there would decide the treatment without evidence. |
| DL-005-14 | A conversion target may carry a zero allocation weight; B3's own record decides the split. (2026-09-19, #129.) | Value-weight the split by market price at the conversion date; record the redemption as an amortização; leave the row invalid
|
B3 converted 175 CPLE6 into 175 CPLE3 and 175 of a redeemable CPLE7 it cashed out a week later at a stated 0,775. B3 states every quantity and no cost split — an Atualização carries no price — so the fix must reproduce the quantities and fill only the blank. Zero is not an arbitrary pick: it is the reading BR-007-05 already takes of a quantity B3 states without a value, as a bonificação does. Value-weighting fills B3's deliberate blank with a market price found nowhere in the extract, and buys nothing — the total realised gain across the redemption and an eventual CPLE3 sale is identical either way, so the weight moves only when it is recognised. An amortização contradicts a quantity B3 did record, since B3 priced a disposal of 175 units and an amortização moves none, and would mean amending BR-005-18's map for every priced Resgate in the ledger. Leaving the row invalid records nothing and cannot be settled by hand: only unclassified rows offer a classify form. Not covered: where B3 states a value for each target, a definition weight is still the only split available — no per-row attributed value is read. |
| DL-005-15 | A ratio with no published factor anywhere may be confirmed by two or more positions agreeing. (2026-09-19, #120.) | Add B3's funds endpoint as a second factor source; accept the derived ratio whenever no factor is found; leave every such row to hand classification | B3 publishes corporate-event factors for listed companies only. Checked live while diagnosing #120: the funds endpoint answers for BCFF but its stockDividends is empty, and BIDI, delisted since its 2022 Nasdaq migration, is empty too. So a second source fixes nothing — for a fund or a delisted issuer there is no published factor to be had, ever, and every split of theirs refused no_factor permanently. Accepting the derived ratio whenever a factor is merely absent would remove the guard exactly where the basis is least verifiable: it is what caught a real grupamento deriving 0,5 against a published 0,1. Requiring the issuer's list to be empty outright keeps that case refusing, because MGLU does publish. Two positions deriving the same ratio from different bases is independent evidence — the observed FII desdobro derived 8 from 16 → 128 and 8 from 49 → 392 — where one position restating its own arithmetic is not. Hand classification remains the fallback and is what a single-position case still needs. Not covered: an issuer with published factors but none in the window, which keeps refusing no_factor by design. |
| DL-005-16 | One institution is identified by an explicit alias table over a mechanical normalisation, never by similarity. (2026-09-20, #136.) | Normalise legal-form suffixes away; match on shared tokens or a common prefix; key positions by institution name instead of id; leave the split and merge at read time | B3 spells one institution several ways across extracts and periods, and a position is keyed by (asset, institution) (SPEC-007 BR-007-08), so each spelling was its own custody location: the owner's ledger held 14 assets on both sides of a split, and WEGE3's Desdobro refused no_basis against a position of zero while its own buys sat on the other spelling. No mechanical rule joins INTER DTVM LTDA to INTER DISTRIBUIDORA DE TITULOS E VALORES MOBILIARIOS LTDA — an acronym and its expansion — without also merging CLEAR CORRETORA - GRUPO XP into XP and BANCO INTER S/A into Inter's DTVM, both of which are genuinely different institutions holding genuinely different custody. A wrongly merged pair is worse than the split it fixes: the split is at least visible as two rows, and reversible. So the table is explicit and reviewed, the mechanical part is limited to what carries no meaning (case, accents, punctuation, whitespace), and a spelling the table does not name creates its own institution, visibly. Not covered: a new expansion of a name already in the ledger still splits until it is named in the table and the rows are migrated. |
| DL-005-17 |
A price-less Transferência pair at one institution carries the position's own average, and neither leg is written without the other; ENBR3L is joined to ENBR3 by an explicit code alias. (2026-09-20, #135.)
|
Classify both legs ignored as a mirror B3 owns the other side of; leave the credit unclassified and keep applying the debit; infer the auction ticker from a <code>L suffix rule; make it an asset conversion (BR-005-20c) |
B3 recorded the July 2023 Energias do Brasil buyout as a price-less debit and credit at one broker — 101 at Clear, 99 at Inter — and BR-005-20a's "other than the credit's" formed no pair. The credit took no cost and stayed unclassified while the debit, needing none, applied: the position went to zero and 200 shares left patrimônio with nothing in the product to say so. Carrying and ignoring net to identical figures — removing q at the average and adding q back at it leaves quantity, total cost and average untouched — so the choice is on other grounds. ignored is a staging classification, decided before the ledger is loaded, which cannot see a pair split across two files and would change the key of rows already stored; the carry is decided at commit, where both the ledger and the batch are visible, and it reuses BR-005-20's existing promote-in-place path to settle the owner's stored credits without touching the applied debit. It invents nothing: the figure is the position's own average, which the ledger already holds. The all-or-nothing guard is the half that would have surfaced this. One leg of a same-position pair applied alone is not a partial import but a loss, and refusing the debit costs nothing — no row is written and no occurrence taken, so the file applies once the credit can take its cost. A cross-institution debit is deliberately left alone: there the shares genuinely left that broker, and BR-005-20a has always let the credit wait for history not yet imported. The ENBR3L alias is listed by hand for the reason #115's was and #136's is: a <code>L rule would silently capture any real ticker ending in L, and a wrongly merged pair joins two instruments into one position with a plausible average and nothing to say it is wrong. It is not a conversion: BR-005-20c moves cost between two assets a user holds in sequence, while this is one instrument B3 named twice in the same week. A carried cost is written at NUMERIC(20,8). Found while pinning the owner's own repeating average, 2.074,64 ÷ 101: the commit cached a position folded from a full-precision carry that the column cannot hold, so verifyPositions read permanent drift against the same rows read back — on every price-less transfer whose average repeats, not only a same-institution one. Rounding once, where the credit is written, is what makes the cache equal its replay; the residual it leaves is what eight places can say about a repeating division, and it is the same figure on both sides. The guard reads the relation, not a formed pair (review finding): asked of pairTransfers' one-to-one matches alone it let through exactly the shapes where no pair forms — two same-position pairs of one quantity, and a same-institution debit competing with a cross-institution one — and in both every debit applied and the position went to zero. The ambiguity guard that protects the cost was removing the guard on the quantity; whether a credit found a source is a different question from whether its position may be emptied without it. It also runs before the exclusion ladder, because a round that gave up a sale on that position never reconsiders it and the batch then called that sale applicable on every re-import. Not covered: a debit an earlier import already applied while its credit stayed unclassified is not retracted — the guard refuses a debit this commit would write, and the ledger it is protecting is repaired by re-importing the file so the credit carries. Commit-time conversion and corporate-event matching still reads B3's raw assetCode rather than the canonical one; no aliased code appears in Movimentação today, so nothing misfires, but a future alias on a code that does would need those matchers to resolve the same way the catalogue does. The rounded carry is not conserved exactly, and deliberately stays that way (2026-09-21): the debit removes the source's full-precision cost while the credit receives price × quantity at eight places, so each hop loses at most quantity × 0,5 × 10⁻⁸ — and only once, since the stored price terminates and every later hop carries it exactly. On the owner's ledger that is 2,5 × 10⁻⁷ on INBR32 (its sale realises 384,50000025) and under R$ 0,00013 across all 55 carried transfers; reaching a centavo takes some two million units in a single transfer. Persisting an exact total on transfer legs, as BR-005-20c's conversion legs do, would mean relaxing the transactions conversion-pairing check on the largest table (expand/contract), a new re-carry comparison to keep BR-005-17's no-op re-import, and a rule for editing such a transfer — cost out of all proportion to a figure no report or tax amount can show. |
| DL-005-18 |
A same-date Desdobro + Grupamento pair resolves as one ordered pair, and a fraction removed between them sells at the second event's scale. (2026-09-21, #139.)
|
Keep refusing every same-date pair; order the pair by the factors' última data com; apply the pair's net ratio as one event; add an intraday sequence to replay ordering | B3 recorded Telefônica Brasil's April 2025 re-denomination as a Desdobro of 237 and a Grupamento of 3,75 on one date, then a 0,75 fraction, and publishes both factors (7900, 0.025) with one última data com — so the date cannot order them, and refusing left VIVT3 90 shares below B3. The stated quantities can: grupamento first, 150 × 0,025 = 3,75; the fraction out; desdobro on 3, 3 × 79 = 237; 240, B3's count — and the other order gives 11.850 ≠ 237. Requiring one exact sequence keeps DL-007-08's posture: two independent sources agreeing, never a best fit. A net ratio (1,6 here) is position-specific and published nowhere. Replay has dates, not times, and ranks every ratio event before the day's sales, so the fraction's sale meets the position after both events; written at that scale (60 @ 26,6419625 instead of 0,75 @ 2.131,357) every derived figure — proceeds, cost removed, realised gain, final count — is the real sequence's, exactly, without reordering replay for every other position. The row's natural key still carries B3's 0,75. |
| DL-005-19 | Reconciliation compares the union of the snapshot and the open ledger positions it covers; coverage is the classes the file carries a row of, at the institutions it names. (2026-09-21, #145.) | Compare only the file's own rows (the prior behaviour); treat every class the parser knows as covered; cover classes but not institutions | Compared row by row, a holding B3 no longer lists was never compared, and four phantom positions on the owner's ledger (WIZS3, TRPL4, ODPV3, MALL11 — the old sides of ticker changes, #143) read as reconciled while counting in portfolio value. B3 writes one tab per class the account holds and none for a class it does not, so an absent tab cannot be told apart from a class filtered out of the export or never covered; reporting it would flood the report with false phantoms, so a class counts as covered only when the file carries at least one row of it. An institution the file never names may be custody the extract does not cover, and is left out on the same reasoning. Classes and institutions are independent sets — every covered class at every named institution — because B3 is the depository for all of them. Classes are the catalog's: stated by any Posição that ever listed the asset (#108), otherwise guessed from the ticker by Movimentação, so a ledger-only position is judged by its best-known class. A row the parser drops (an unrecognised tab, a Renda Fixa product that is not CDB/LCI/LCA) is indistinguishable from an absent one, which is why accepting B3's zero is refused for this cause (BR-005-25): a false absence costs a line in the report, never a holding. The costs: a phantom in a class the file carries no read row of stays invisible until the file does; a position first traded after the reference date is left out rather than reported. |
| DL-005-21 | A fund liquidated into another fund's shares is a sale of the old fund at its liquidation value and an acquisition of the new shares at their stated cost. Both figures are the administrator's published values, carried by an explicit liquidation definition. The four ticker renames are plain conversions. (2026-09-21, #143; reverses the same day's return-of-capital reading.) | A cost-carrying conversion that keeps B3's cash as a return of capital (shipped in #149, then reversed); a pro-rata partial sale; B3's priced Resgate read as a sale at the cash price alone (the prior behaviour) |
BPFF11 and HGFF11 were liquidated under Art. 18 Lei 8.668/1993. Each share was redeemed at a Valor de Liquidação — BPFF11 R$ 62,03538245, HGFF11 R$ 71,04670108 — paid partly in RVBI receipts and partly in cash, and taxed at 20 % above the holder's cost. The RVBI shares received carry an acquisition cost of R$ 64,15 each for IR and any later gain (fatos relevantes of 02/10/2025, fnet 1005408 and 1005407). B3's extract prices only the cash part, so reading the Resgate as a sale at that price realised the whole basis as a loss. Keeping the cost and treating the cash as a return of capital hid a real loss and overstated the new cost basis. The administrator is the tax-responsible party and states both missing figures, so an explicit definition may carry them. It is the same principle as DL-005-14: fill B3's blank only with a figure an authoritative party states, never with one inferred. The costs: the flows through the eight days between B3's credit date and the redemption date show a contribution and a withdrawal the owner never made; and the net flow differs from B3's stated cash by the administrator's sub-cent fractions (R$ 0,25 on the owner's ledger). |
| DL-005-20 | n price-less credits and n debits on one position, date and quantity pair as n no-ops. (2026-09-21, #145.) | Keep refusing every shape where a credit sees two debits; pair only after a user classifies one leg by hand | On 2026-09-18 B3 wrote, at Inter, two price-less debits and two credits of one quantity on BBAS3 (455), BRSR6 (367), EGIE3 (630) and KLBN11 (786) — a round trip inside the broker the owner never made. BR-005-20a's ambiguity guard formed no pair, so every import left 16 rows needing attention on positions that already matched B3. The guard exists to avoid choosing a source; here there is nothing to choose, because every leg is the same position and every pairing produces the same carry and the same net zero. The carry reads the position before any leg of the round trip — the other legs are neither blockers nor history — which is what lets a trip moving more than half the holding (455 of 800) carry at all. The quantity guard is unchanged: where the credits cannot carry, every debit is still held back. |
| DL-005-22 | A subscription B3 states no price for is priced at the main asset's close on the credit date, marked as an estimate. (2026-09-21, #144.) | Ask the user for the price at classification; zero cost; read the offer price from the fund's CVM filing | The owner ruled out being asked. Zero cost understates preço médio and overstates every later gain. CVM filings are exact but a new integration that covers FIIs only. The close is automatic and close to the offer price (XPML11: R$ 114,90 close against R$ 112,95 paid), and the marker keeps it honest. The nearest earlier close is used however old (owner decision). Dated the credit date because custody changes then. Closes are fetched before commit by the worker, never inside the commit transaction, which would hold ledger locks across an HTTP call. Never repriced once applied, so shown figures stay stable. |
| DL-005-23 |
A whole-position Atualização is a refresh and ignored. (2026-09-21, #144.)
|
Leave it unclassified; drop it |
Left unclassified it sits in "Needs attention" with no correct user action — any classification corrupts the ledger. Dropping breaks BR-005-19. The rule is deterministic, runs after every resolver that could claim the row, stays hand-classifiable, and Posição reconciliation catches a wrong call. |
| DL-005-24 |
A Tesouro Produto is read by its words against a hand-listed product table. (2026-09-27, #164.)
|
Exact match on the expected spelling; an alias row per spelling seen | The asset must meet the price series from the first import, and B3's spelling of Tesouro Educa+ and Renda+ has not been seen in any extract. An exact match would leave any other spelling unpriced until a data migration; a per-spelling alias has the same gap. Words are bounded by the table — one indexer, coupon words, the product's own name words — and any unknown word means no match, so a product the table does not know stays visibly unpriced instead of being priced from one it resembles. |
| DL-005-25 | A hand-classified subscription credit is offered, never overwritten: its exercise is cleared when the classification carries a cost, and offered a re-type to an estimated subscription when it carries none. (2026-09-28, #157.) | Leave the exercise in "Needs attention" with no correct action; supersede the exercise whenever a hand-classified credit matches, whatever its cost; re-type the credit to an estimated subscription automatically | IN RFB 1.585/2015 art. 58 sets a share's cost at what was paid. A bonificação's cost is the capitalised profit or reserve (§1), and zero applies only to splits, free founder's shares, or an asset no criterion can value (§7). Where the paid price is absent, the cost is the current value on the acquisition date (§3 IV), which is the close DL-005-22 already uses. A paid subscription entered as a zero-cost bonificação therefore understates preço médio, and on an FII overstates a gain taxed at 20% (Lei 8.668/1993 art. 18). The owner's HGLG11, MGFF11 and VISC11 credits were classified that way before BR-005-20d existed. Superseding the exercise regardless would hide that cost from the only surface that shows it. Re-typing automatically would override a hand classification, which BR-005-20 and 20b never do, and a zero-cost bonificação can be genuine. The offer asks the user to confirm, not for a price (#144 D1). A costed classification is cleared without asking because the cost question is already answered. Researched and recommended, then acknowledged by the owner. |
| DL-005-26 | A Posição discrepancy is attributed to post-reference or unsettled trades only when removing exactly those buys and sells reproduces B3's quantity; missing history requires the recorded position never to have reached B3's figure. (2026-09-29, #146.) | Treat every ledger deficit as missing history; infer from any nearby trade; compare the ledger at trade date and silently discard the discrepancy | SEER3 read 0 in the ledger against 130 at B3 after two complete sales on Friday 2026-09-11. The Posição was exported on Monday 2026-09-14, before D+2 settlement on Tuesday, but committed with the default date 2026-09-21. The ledger held every acquisition, so “missing history” contradicted the evidence. A trade can explain the difference only when its exact quantity does: removing buys and sells after the confirmed date or still awaiting D+2 must reproduce the snapshot. The historical peak separates an incomplete ledger from a fully recorded position later reduced by another event without guessing why. Keeping the discrepancy visible matters because it also exposes a wrong confirmed date; replaying only to that date would hide the user's input error. The prompt now says that Posição is settled custody at export and names the date to enter. Unclassified ledger rows and an asset absent from the snapshot keep their earlier priority because each is stronger evidence than trade timing. |
| DL-005-27 | A seven-segment import key identifies the B3 source row across later ledger re-typing. (2026-09-29, #178.) | Keep the internal type in a price-less mapped row's editable identity, as #110 did; rederive every edited row from its current ledger fields | #110 distinguished an unmapped key's placeholder type from a price-less mapped key's resolved type and treated only the first as classification. That made a later transfer_in → buy correction rederive the second into a six-segment key no form of the original B3 row can produce, so the next import inserted the source row again and doubled its shares. In both seven-segment forms the raw B3 movement suffix identifies the source observation; the internal type is the ledger interpretation, just as a carried or typed price is. Asset, institution, date and quantity still identify a different source row and therefore still rederive under BR-006-04. |
| DL-005-28 | A resolved subscription exercise stays visible but cannot be classified again. (2026-09-30, #179.) | Offer an undo of the resolution; keep the classify form with a warning | Every classification of the consumed exercise would add its quantity to the right ticker while the main-asset credit already carries it. A warning leaves that corrupting action available. An undo must reverse the exercise and credit as one guarded operation, beyond this correction. The user can correct the active credit through SPEC-006. The owner chose to hide the form and reject direct requests. |
| DL-005-29 | Recover exact cash totals while retaining source identity. (2026-10-01, #158.) | Leave a stated cash payment unpriced; key on the recovered unit price; round a repeating quotient | B3 may state the operation total while omitting the unit price. Only cash earnings with an exact storable quotient recover it. Keys continue to describe the source row, so unequal recovered totals keep the original occurrence ordinals and an earlier import is corrected rather than repeated. Original-row provenance distinguishes an explicitly stated zero price from a missing-price payment, regardless of import order. A figure requiring rounding remains unclassified; no purchase cost or corporate-event trail is inferred. |
| DL-005-30 |
Cash custody fees are visible as ignored, not ledger transactions. (2026-10-02, #159.)
|
Keep unclassified; book a sale or negative income; introduce a cash-fee ledger type |
Cobrança de Taxa Semestral moves no quantity or cost basis and is not a provento. The specs do not model fees outside trades. The existing ignored path preserves the source row and supersedes untouched historical copies on re-import without inventing a financial effect. Map v8 follows #158’s already-shipped v7. |
| DL-005-31 | An unclassified row can be ignored by the user, with a reason. (2026-10-09, SPEC-022.) | Keep such rows pending indefinitely (#158 D9's other option); delete the row | Seven rows in the owner's queue are documented non-position statements that no rule should activate (#158 D5–D8). With no action available they would stay in "Precisa da sua atenção" for good, which trains the user to ignore the queue. Deleting a row loses the evidence. A stated reason keeps the decision auditable and reversible. |
| DL-005-32 | Open discrepancies are measured again against the current ledger, as of the Posição date. (2026-10-09.) | Rewrite the stored report on every re-derivation; compare against today's position | The personal instance showed 7 open divergences while 4 already agreed (ABCB4, AFHI11, HSML11, XPML11), because #192–#196 had re-derived the ledger after the 2026-09-27 commit. Comparing today's position would flag every later trade as a divergence. Rewriting the stored report would erase what the import found. |