Count hand-valued assets into the wealth figure

A wealth figure that ignores the house is not a wealth figure. Assets
without a market feed - a house, a car, a private loan - are now added
by hand on the Wealth page with a stated value, a currency and the day
the estimate was made; a negative value records a liability. They are
registry entities in assets.finance like everything else, join the
per-currency totals immediately, and a currency held only in an asset
earns its own line.
This commit is contained in:
Lars Nolden
2026-09-14 09:29:19 +02:00
parent a1480af74d
commit 77f4ea5655
15 changed files with 564 additions and 29 deletions
+47 -9
View File
@@ -15,8 +15,11 @@ import (
// same journal derives.
type Wealth struct {
Accounts []WealthAccount `json:"accounts"`
// Totals is cash, position value and their sum per currency, across every
// account.
// Assets are the hand-valued possessions outside any account, echoed here
// so the page that shows the total also shows what the total contains.
Assets []WealthAsset `json:"assets"`
// Totals is cash, position value, hand-valued assets and their sum per
// currency, across every account.
Totals []WealthTotal `json:"totals"`
}
@@ -28,8 +31,11 @@ type WealthTotal struct {
// in Unpriced, because valuing them at cost would report a number the
// journal cannot support.
Positions domain.Money `json:"positions"`
Wealth domain.Money `json:"wealth"`
Unpriced int `json:"unpriced"`
// Assets is the stated value of every hand-valued asset in this currency,
// and Wealth is cash, positions and assets together.
Assets domain.Money `json:"assets"`
Wealth domain.Money `json:"wealth"`
Unpriced int `json:"unpriced"`
}
// WealthAccount is one account's position as the journal records it.
@@ -113,6 +119,17 @@ type WealthHolding struct {
Records int `json:"records"`
}
// WealthAsset is one hand-valued asset as the journal records it. The value is
// stated, never quoted, and carries the day it was stated.
type WealthAsset struct {
AssetID string `json:"asset_id"`
Name string `json:"name"`
Kind string `json:"kind,omitempty"`
Currency string `json:"currency"`
Value domain.Money `json:"value"`
ValuedAt string `json:"valued_at"`
}
// WealthCheck is one named verification with its evidence. Failed marks a
// disagreement inside the journal; the rest are notes that explain a figure
// before it is compared with a broker's screen.
@@ -282,11 +299,18 @@ func WealthOf(data domain.Dataset) Wealth {
}
}
report := Wealth{Accounts: []WealthAccount{}, Totals: []WealthTotal{}}
report := Wealth{Accounts: []WealthAccount{}, Assets: []WealthAsset{}, Totals: []WealthTotal{}}
totals := map[string]int64{}
positionTotals := map[string]int64{}
assetTotals := map[string]int64{}
unpricedTotals := map[string]int{}
currencies := []string{}
seen := func(currency string) {
if _, ok := totals[currency]; !ok {
currencies = append(currencies, currency)
totals[currency] = 0
}
}
for _, account := range data.Accounts {
st := state(account.ID)
kind := account.Kind
@@ -308,9 +332,7 @@ func WealthOf(data domain.Dataset) Wealth {
})
}
}
if _, seen := totals[account.Currency]; !seen {
currencies = append(currencies, account.Currency)
}
seen(account.Currency)
totals[account.Currency] += st.cash
positions, unpriced, stale := int64(0), 0, []string{}
for _, id := range st.order {
@@ -391,11 +413,27 @@ func WealthOf(data domain.Dataset) Wealth {
}
report.Accounts = append(report.Accounts, entry)
}
// Hand-valued assets join the totals after the accounts: they belong to no
// account, and a currency held only in an asset still earns its own line.
for _, asset := range data.Assets {
value, err := asset.Value.Minor()
if err != nil {
continue
}
seen(asset.Currency)
assetTotals[asset.Currency] += value
report.Assets = append(report.Assets, WealthAsset{
AssetID: asset.ID, Name: asset.Name, Kind: asset.Kind,
Currency: asset.Currency, Value: domain.FormatMoney(value), ValuedAt: asset.ValuedAt,
})
}
slices.SortStableFunc(report.Assets, func(x, y WealthAsset) int { return strings.Compare(x.Name, y.Name) })
for _, currency := range currencies {
report.Totals = append(report.Totals, WealthTotal{
Currency: currency, Cash: domain.FormatMoney(totals[currency]),
Positions: domain.FormatMoney(positionTotals[currency]),
Wealth: domain.FormatMoney(totals[currency] + positionTotals[currency]),
Assets: domain.FormatMoney(assetTotals[currency]),
Wealth: domain.FormatMoney(totals[currency] + positionTotals[currency] + assetTotals[currency]),
Unpriced: unpricedTotals[currency],
})
}