Balance reconstruction
Summary
Section titled “Summary”A holding’s past balance is not stored. It is computed on demand by
the balance engine, balanceAt(evidence, at), from two things the
holding records:
- its readings: balances a feed or a person reported at an instant (observations);
- its ledger: the transactions that moved it.
BalanceAtTimeService.getBalance(holdingId, at) is the entry point
for one holding at one instant. The same engine
writes today’s holdings.balance, so the past and the present come
from one rule.
Which readings count
Section titled “Which readings count”Not every reading anchors a balance. Which ones do depends on the holding’s kind:
- A feed holding is one an exchange, wallet, broker or statement feeds. Its feed’s checkpoints anchor it. A value a person types on it is kept as a verification: it is compared with the feed’s figure and never anchors.
- A snapshot holding is one a person keeps. Their snapshots anchor it.
On a feed holding, a snapshot taken for a stretch no feed covers can still anchor that stretch.
How a balance is found
Section titled “How a balance is found”For a holding and an instant at:
- Before the holding starts, it is absent.
balanceis null andbeforeRecordsis true. The start is the earliest of the holding’s creation, its first kept reading and its first ledger entry. - After a reading, walk forward. Take the latest anchoring reading
at or before
atand add every ledger entry since it. This is the usual case (anchor: 'observation-before'). - Before the first reading, walk back. Take the first anchoring
reading and subtract the entries between
atand it (anchor: 'observation-after'). A snapshot holding holds its first value back to its start instead, so its balance cannot jump where its history begins. - With no reading at all, sum the ledger from the start
(
anchor: null).
interface BalanceAtTimeResult { balance: Decimal | null; anchor: 'observation-after' | 'observation-before' | null; anchorAt: Date | null; beforeRecords: boolean;}No line and no floor
Section titled “No line and no floor”- No line across a gap. Between two readings only the ledger moves the balance. When the second reading disagrees with the first plus the ledger, the balance steps at the second reading. The money side books that step as unexplained drift on the same instant, so flows and the value history agree.
- No floor. A balance goes negative when the ledger says so, usually because an import starts partway through a holding’s life. It is shown as computed rather than clamped to zero, so the gap stays visible and can be answered.
How the rollup uses it
Section titled “How the rollup uses it”The nightly portfolio-value rollup calls
balancesFor. It loads each holding’s readings and ledger once, 25
holdings at a time, and answers every day of the window from memory.
An ad-hoc caller, such as a chart request, calls getBalance and lets
the service load what it needs.
What it doesn’t do
Section titled “What it doesn’t do”- It does not write. The ledger, the readings and the stored balance are untouched.
- It does not price. The balance is in the token’s units (BTC, AAPL, EUR, …); the rollup values it against the price graph.