Start and end balance anchors for historical account balances #2455

Merged
zachgoll merged 24 commits from zachgoll/valuation-anchors-v2 into main 2025-07-15 23:42:41 +08:00
zachgoll commented 2025-07-11 07:07:42 +08:00 (Migrated from github.com)

Second attempt at #2449, scoped down significantly to focus solely on valuation anchors. Other work from #2449 will be re-introduced in follow-up PRs.

CleanShot 2025-07-14 at 10 19 06

Account anchors overview

Up to this point we've calculated balances and holdings from the entries table as a "pseudo event-sourced" data model, but there are a few missing pieces here:

  • No domain rules establishing / managing the start/end "valuation anchors" for an account (essentially "snapshots" set for an account to work around the fact that we do not have 100% transaction/trade history always)
  • Inconsistent treatment/modification of "Cash" vs. "Non Cash" account balances

Event sourced ledger

The major goal of this PR is to move to a model where instead of using account.balance and account.cash_balance (which are really just "cache fields" derived from entries), we have a complete "ledger" of entries that can construct an entire historical balance timeline without any external inputs. Adding balance and cash_balance to the Valuation model gives the necessary granularity to set an "opening balance" that has all the balance components required to initialize the timeline.

By doing this, balances and holdings tables become solely "cache tables" (materialized) that can be deleted/re-constructed by applying an algorithm against the "ledger".

This PR does not contain the concept of "Holding Snapshots" (i.e. starting point for an account's portfolio), but that will come in a future PR.

Cash vs. Non Cash balance calculation

A big improvement in this PR is an update to the balance calculation algorithm and Valuation model to acknowledge both "Cash" and "Non Cash" balances. Depending on the accountable type, transactions and trades will modify different components of an account's balance.

More formally, this could be described in balance sheet terms as "current" vs. "long term" assets/liabilities. For example, Cash equivalents (short term asset) vs. PP&E (long term asset); Short term debt vs. Long term debt. This designation describes how quickly and easily a balance can be liquidated / paid down.

Below is a table showing each of our account types and their designations:

Accountable Liquid component (cash) Illiquid component (non cash)
Depository Entire balance (cash) 0
Investment Brokerage cash Holdings market value
Crypto 0 Crypto value
Property 0 Property value
Vehicle 0 Vehicle value
OtherAsset 0 Asset value
CreditCard Card balance (debt) 0
Loan 0 Loan principal (debt)
OtherLiability 0 Liability balance
  • All Cash (Depository, CreditCard)
    • Can have transactions, valuations
    • Cash balance == Total Balance always
  • Non Cash (Property, Vehicle, OtherAsset, OtherLiability, Loan)
    • Non Cash Balance == Total Balance always (i.e. cash_balance is 0)
    • Transactions only impact balance on Loan accounts (reduces principal). Otherwise, transactions don't influence balance and balance reconciliations are the primary way to update the balance.
  • Hybrid (Investments, Crypto)
    • Cash component equals "brokerage cash"
    • Non cash component equals "Holdings value"
Second attempt at #2449, scoped down significantly to focus solely on valuation anchors. Other work from #2449 will be re-introduced in follow-up PRs. <img width="1372" height="353" alt="CleanShot 2025-07-14 at 10 19 06" src="https://github.com/user-attachments/assets/a47eafe5-1469-421d-a86b-d66acb679e7f" /> ## Account anchors overview Up to this point we've calculated balances and holdings from the `entries` table as a "pseudo event-sourced" data model, but there are a few missing pieces here: - No domain rules establishing / managing the start/end "valuation anchors" for an account (essentially "snapshots" set for an account to work around the fact that we do not have 100% transaction/trade history always) - Inconsistent treatment/modification of "Cash" vs. "Non Cash" account balances ### Event sourced ledger The major goal of this PR is to move to a model where instead of using `account.balance` and `account.cash_balance` (which are really just "cache fields" derived from entries), we have a complete "ledger" of `entries` that can construct an entire historical balance timeline without any external inputs. Adding `balance` and `cash_balance` to the `Valuation` model gives the necessary granularity to set an "opening balance" that has all the balance components required to initialize the timeline. By doing this, `balances` and `holdings` tables become solely "cache tables" (materialized) that can be deleted/re-constructed by applying an algorithm against the "ledger". This PR does not contain the concept of "Holding Snapshots" (i.e. starting point for an account's portfolio), but that will come in a future PR. ### Cash vs. Non Cash balance calculation A big improvement in this PR is an update to the balance calculation algorithm and `Valuation` model to acknowledge both "Cash" and "Non Cash" balances. Depending on the accountable type, transactions and trades will modify _different components_ of an account's balance. More formally, this could be described in balance sheet terms as "current" vs. "long term" assets/liabilities. For example, Cash equivalents (short term asset) vs. PP&E (long term asset); Short term debt vs. Long term debt. This designation describes how quickly and easily a balance can be liquidated / paid down. Below is a table showing each of our account types and their designations: Accountable | Liquid component (cash) | Illiquid component (non cash) -- | -- | -- Depository | Entire balance (cash) | 0 Investment | Brokerage cash | Holdings market value Crypto | 0 | Crypto value Property | 0| Property value Vehicle | 0| Vehicle value OtherAsset | 0 | Asset value CreditCard | Card balance (debt) | 0 Loan | 0 | Loan principal (debt) OtherLiability | 0 | Liability balance - All Cash (Depository, CreditCard) - Can have transactions, valuations - Cash balance == Total Balance always - Non Cash (Property, Vehicle, OtherAsset, OtherLiability, Loan) - Non Cash Balance == Total Balance always (i.e. cash_balance is 0) - Transactions only impact balance on Loan accounts (reduces principal). Otherwise, transactions don't influence balance and balance reconciliations are the primary way to update the balance. - Hybrid (Investments, Crypto) - Cash component equals "brokerage cash" - Non cash component equals "Holdings value"
Sign in to join this conversation.