Portfolio value rollup
Summary
Section titled “Summary”portfolio_value_daily is a derived daily cache of portfolio
totals, keyed by (user, scope_kind, scope_id, snapshot_date, base_currency). It holds user-wide totals and per-institution /
per-account / per-holding scoped series so detail-page charts read
from one table instead of three. It’s rebuildable end-to-end from
transactions,
observations, and
prices. The portfolio-value-rollup
job runs in the nightly group, after the price backfill and
transfer linking steps before it have finished.
Schema
Section titled “Schema”Primary key: (userId, scopeKind, scopeId, snapshotDate, baseCurrencyId). Indexes on (userId, snapshotDate desc) and
(userId, scopeKind, scopeId, snapshotDate desc) cover the chart
reads.
Scopes
Section titled “Scopes”All four scopes for a user are computed in the same backfill pass — no separate per-scope jobs.
How it’s computed
Section titled “How it’s computed”For each (user, scope, date):
- For each holding in scope, take its balance on that date from
BalanceAtTimeService.balancesFor. It loads each holding’s readings and ledger once, 25 holdings at a time, so no day goes back to the database. - Price each balance in the base currency at the day’s close, from
one
PriceReaderseries loaded once per call, for every holding at every day of the chunk that call computes. - Sum the priced balances. Bucket each holding into a
coverageQualitybased on whether the balance reconstruction had a strong anchor and whether the price was fresh / hub-routed / stale. - Compute
costBasis,realizedPnl,unrealizedPnlfrom the ledger lots up todate. UPSERTthe row.
The job is idempotent per (user, scope, date, base) — re-running just overwrites.
The holding-inclusion rule
Section titled “The holding-inclusion rule”A holding counts toward a total when it is shown or was hidden by the
closed-position sweep, it is active, and its token is not a scam for its
owner. That is the canonical
holding-inclusion rule. The scam
check is in the query that loads a user’s holdings; the rest is
holdingCountsInTotal:
export function holdingCountsInTotal(holding: InclusionHolding): boolean { if (holding.isHidden && holding.hiddenBy !== 'auto') return false; return holding.isActive;}The user, institution and account rows total the holdings that count.
The rollup stores holding rows for more than those: every holding that
counts, plus the shown holdings that do not count because they are
inactive (holdingIsRolledUp). A shown inactive holding keeps a row with
nothing in it; a holding its owner hid gets no row. Neither does a shown
holding whose token is a scam for its owner: the query that lists a
user’s holdings leaves it out, so the rollup never sees it.
export function holdingIsRolledUp(holding: InclusionHolding): boolean { return !holding.isHidden || holdingCountsInTotal(holding);}The readers that add holding rows up across holdings (the chart
series, a day’s total, returns) apply the counting rule to the rows they
read. It is duplicated in SQL, in the one inclusion clause
(includedHoldingRows) that each of those readers in
PortfolioValueDailyRepository filters through, so the chart read path
and the dashboard headline always agree. A reader that returns a
holding’s own figures applies no rule: findLatestHoldingCostBasis, and
findRange asked for a holding scope, return whatever the rollup
stored for that holding. Keeping the
two definitions in sync is the point — that’s what makes the
chart’s latest point reconcile with the headline.
Coverage quality
Section titled “Coverage quality”The chart UI reads coverageQuality and renders accordingly: a
full series is solid, partial is shaded, estimated is dashed,
unknown becomes a gap.
Triggering a rebuild
Section titled “Triggering a rebuild”- Scheduled. The
portfolio-value-rollupjob runs nightly, as a step of the nightly group. See the Job catalogue. - On import. A user-initiated import triggers a
portfolio-history-backfilluser job for that user, scoped to the affected date range.
See also
Section titled “See also”- Balance reconstruction
- Pricing & the price graph
- Why the holding-inclusion rule lives twice
- Job catalogue —
portfolio-value-rollup,historical-price-backfill,portfolio-history-backfill - Glossary: rollup, coverage quality