Skip to content

Balance reconstruction

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.

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.

For a holding and an instant at:

  1. Before the holding starts, it is absent. balance is null and beforeRecords is true. The start is the earliest of the holding’s creation, its first kept reading and its first ledger entry.
  2. After a reading, walk forward. Take the latest anchoring reading at or before at and add every ledger entry since it. This is the usual case (anchor: 'observation-before').
  3. Before the first reading, walk back. Take the first anchoring reading and subtract the entries between at and 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.
  4. 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 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.

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.

  • 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.