SPEC 013 Portfolio Value Report - rafaelfqueiroz/allmywallet GitHub Wiki

SPEC-013 — Portfolio Value Report (pt-BR: Patrimônio)

Status Draft
PRD source F5.2 (FR-5.8–5.13)
Depends on SPEC-009, SPEC-011
Required by SPEC-019
Milestone M5

Description

The report that answers how much do I have, and where did the growth come from?

Its central purpose is decomposition. A portfolio that grew from R$100k to R$150k tells you nothing until you know whether that was R$50k of deposits or R$50k of returns — those are opposite facts about how the year went. Money that grew because you deposited it must never look like money that grew because it performed. Every other requirement here serves that distinction.

Terminology. The English name is Portfolio Value, not "Net Worth". Net worth means assets minus liabilities, and this product tracks investments only — no debts, no property, no current accounts — so "Net Worth" would systematically overstate what it measures for any user with a mortgage. If liabilities are ever tracked, "Net Worth" becomes correct and this is a rename, not a redesign. The pt-BR interface uses "Patrimônio", which carries no such ambiguity.

Business rules

ID Rule
BR-013-01 Chart total portfolio value over time at daily granularity, aggregated to weekly or monthly for long periods.
BR-013-02 Decompose growth into three distinct drivers: net contributions, price change, and earnings received. This is the report's reason to exist.
BR-013-03 The three drivers must reconcile: closing value − opening value = net contributions + price change + earnings.
BR-013-04 Show current total value, total invested (net contributions), and absolute and percentage gain. In the pt-BR interface total invested is labelled Aportes líquidos, explained in place, so it is never confused with Portfólio's Valor aplicado (the cost of current holdings). (Amended 2026-10-12 — SPEC-022 BR-022-39, DL-022-20.)
BR-013-05 Show value split by the selected grouping (SPEC-011) at any point on the timeline, as a stacked series — so the user can watch the fixed-income share grow relative to equities. Limited to the dimensions the snapshot decomposes, which is asset class alone; for any other dimension the report names the missing history rather than rendering empty or back-projected bands (DL-013-07).
BR-013-06 Show monthly net contribution as a separate bar series, itself breakable by the selected grouping.
BR-013-07 Mark estimated values distinctly from observed market prices. Accrued fixed income is an estimate (SPEC-009, BR-009-11); a chart blending it invisibly with observed prices misrepresents the precision the product has.
BR-013-08 Net contributions count money entering and leaving the portfolio — buys, sells, transfers in and out. They do not count price movement or earnings. A paired transfer — a debit and a credit that SPEC-005 BR-005-20a matches one-to-one — is a move between the user's own custodians and contributes zero, whatever price either leg carries. So does a same-position round trip — a debit and a credit of the same quantity of one asset at one institution on one date — even when B3 prices the credit and BR-005-20a's pairing therefore declines it. Only an unpaired transfer is money in or out: it enters Total investido and Ganho at the cost basis it carries, and everything else — the growth decomposition, the monthly contribution bars (BR-013-06) and SPEC-012's returns (BR-012-01) — at its market value on the transfer date. (Amended 2026-09-29, #181, then #183 — see DL-013-08, DL-013-09, DL-013-10, DL-013-11.)
BR-013-09 Earnings are counted at pay date, consistent with SPEC-012, BR-012-06.
BR-013-10 Historical points use official closing prices, never intraday quotes (SPEC-009, BR-009-02). Only the current value uses a live delayed quote.
BR-013-11 The report reads from DailyValuationSnapshot (SPEC-011, BR-011-13).
BR-013-12 The chart's endpoint total must equal the total shown in the Composition report for the same scope and date.
BR-013-13 Both the valuation as-of date and the last custody import date are displayed (SPEC-005, BR-005-27).

Acceptance Criteria

  • Total portfolio value charts correctly over daily, weekly and monthly granularity.
  • The three growth drivers reconcile exactly: closing − opening = contributions + price change + earnings, asserted automatically across several periods.
  • A period with a large deposit and flat prices shows the growth entirely as contributions and zero as price change.
  • A period with no deposits and rising prices shows growth entirely as price change.
  • A dividend-heavy period attributes the correct amount to earnings and not to price change.
  • Current value, total invested, and absolute and percentage gain are correct and mutually consistent.
  • The stacked series bands asset class and its bands sum to the total line; every other grouping dimension states why it has no history rather than showing empty bands (DL-013-07).
  • Monthly net contribution bars are correct, including negative months (net withdrawals).
  • Accrued fixed income is visually distinguished from observed prices on the chart and in the totals.
  • Historical points use official closes; changing the intraday quote does not move a past point.
  • The endpoint total equals the Composition report total for the same scope and date.
  • Editing a transaction two years back changes the chart from that date forward, not only today.
  • Report runs at portfolio and wallet scope with every grouping (SPEC-011).
  • Valuation as-of date and last import date are both visible.
  • A scope with no holdings renders an empty state.

Out of Scope

  • Return percentages, TWR, XIRR and benchmark comparison (SPEC-012) — this report is about amounts, that one about rates.
  • Liabilities, debts, property, current accounts or any non-investment asset — the reason it is not called Net Worth.
  • Cash balances held at the broker but not invested.
  • Future projections, contribution planning or retirement modelling.
  • Inflation-adjusted (real) portfolio value — real return lives in SPEC-012.
  • After-tax values (SPEC-009, BR-009-12 — figures are gross).
  • Per-transaction drill-through from a chart point.

Decision Logs

# Decision Alternatives considered Rationale
DL-013-01 English name is "Portfolio Value", not "Net Worth". "Net Worth" (what Empower and Kubera call it); "Wealth"; "Total Assets" Net worth has a precise meaning — assets minus liabilities — and this product tracks no liabilities. For any user with a mortgage the figure would systematically overstate what it measures. "Wealth" invites the same overstatement; "Equity" and "Estate" mean other things in finance. Revisit if liabilities are ever tracked.
DL-013-02 Growth decomposed into three drivers, and required to reconcile. Show total value only; show value and contributions Without decomposition the report answers nothing useful — a rising line during a bull market with monthly deposits is indistinguishable from either cause alone. Requiring the three to reconcile arithmetically also makes miscounting a transaction type a failing test rather than a silent wrong chart.
DL-013-03 Earnings separated from price change, not folded into it. Treat received earnings as part of return An income investor's core question is how much the portfolio pays versus how much it appreciates. Folding them together destroys exactly that distinction, and it is the distinction Marina's persona is built around.
DL-013-04 Estimated values marked on the chart itself. Note the caveat once in a footnote A footnote is read once and forgotten; the chart is looked at daily. For a fixed-income-heavy portfolio a large share of the line is accrued rather than observed, and that should be visible where the number is.
DL-013-05 Historical points use closes; only "now" uses the live quote. Use the latest quote throughout Otherwise a past point on the chart would move depending on what time of day the page was loaded — the same historical fact rendering differently, which destroys confidence in every other number.
DL-013-06 Endpoint must equal the Composition total. Let each report compute independently Two reports disagreeing about how much money the user has is the single most trust-destroying defect this product could ship. Making it an explicit cross-report assertion catches it in CI rather than in a support ticket.
DL-013-07 The stacked series bands asset class only, and names the absence for the other four dimensions. Band every dimension by storing a per-day breakdown along each; back-project today's allocation over past days daily_valuation_snapshots.by_asset_class is the only historical breakdown stored, and a wallet is not a property of an asset — allocation changes over time, so a per-wallet band asks what the split was on each past day. Back-projecting today's split would rewrite every historical chart the moment someone reassigns an asset, and a rebuild would then disagree with the snapshot it replaced (SPEC-009 DM-4). Storing it means rows multiplying by each dimension's cardinality over every day of history, on the rebuild path, expand/contract, against a production database with no staging. Issue #50 weighed that and closed it as a decision on 2026-08-18: wallet scope means current composition over portfolio-level history. The report therefore returns NO_HISTORICAL_BREAKDOWN and says so — an empty chart reads as "you held nothing", which is a false statement about someone's portfolio, where a named reason is a true one about the product. Revisit if the PRD ever commits to per-wallet history as a headline capability.
DL-013-08 A transfer is a flow at the cost it carries, so an inter-custodian pair nets to zero. (2026-09-29, #181.) Value the debit at its stated price; exclude transfers from flows entirely B3 records a move between brokers as a price-less debit and a credit at carried cost (SPEC-005 BR-005-20a). Valuing the debit at its stated price, zero, counted every such move as a deposit: R$ 193.802,75 on the owner's ledger, which turned a real gain into a −R$ 98.779 Ganho and read as a loss in TWR on each transfer date. Moves between accounts of the same portfolio are not external cash flows, while an in-kind transfer from outside is one (GIPS). Excluding transfers outright would get the pair right but read a one-sided incoming transfer as profit. Valuing both legs at carried cost gets both cases right.
DL-013-09 A paired transfer contributes zero whatever its legs' prices; an unpaired one is valued at cost for Total investido and at market value for returns. (2026-09-29, #183, amending DL-013-08.) Keep valuing each leg at the cost it carries (DL-013-08); value unpaired transfers at cost everywhere DL-013-08's per-leg rule fails its own intent in two cases, found in the #181 review. A credit with a price B3 stated (#145) or a stored fallback cost (#112) does not equal the debit's carried cost, so the pair books a deposit that never happened: 100 carried at 10,00 and credited at 12,50 adds 250 and reads −22,7 % TWR for the day. Pairing is the fact that makes a move internal, so pairing, not price, decides the flow. For an unpaired leg, GIPS values an in-kind external flow at market value on the transfer date. Valuing it at cost reads 100 shares leaving at cost 10,00 with a close of 40,00 as a −75 % day. Total investido stays at cost, because that is what "invested" means to a user reconciling against a broker. The two figures therefore diverge only for unpaired transfers, and must be stored separately.
DL-013-10 The growth decomposition and monthly bars value an unpaired transfer at market; only the Total investido / Ganho headline uses cost. (2026-09-29, #183.) Decomposition and bars at cost, consistent with Total investido The decomposition's price change is a residual: closing − opening − contributions − earnings. Take 100 shares that arrive carried at 8,00 with a close of 11,00. At cost, their R$ 300 of appreciation before the user tracked them lands in this portfolio's price change, which then says the holdings earned something they did not. At market value, price change measures only movement while held, which is what DL-013-02 asks the three drivers to separate. GIPS values in-kind contributions the same way. The headline stays at cost because Total investido answers "how much did I put in", which a user reconciles against broker statements.
DL-013-11 A same-position round trip is internal whatever the credit's price. (2026-09-29, #183 review, follow-up #186.) Leave such round trips to BR-005-20a's pairing, which pairs only price-less credits Shares that leave a position and return to it on the same day moved no money. BR-005-20a declines a priced credit because pairing decides whose cost the credit carries, a question a stated price already answers. Whether money entered the portfolio is a different question. Leaving it to the pairing let a priced round trip move Total investido with nothing moving: +0,16666665 in the #183 fixture.