Skip to content

holdings: new command showing investment holdings and performance - #2683

Merged
simonmichael merged 26 commits into
mainfrom
holdings
Aug 6, 2026
Merged

holdings: new command showing investment holdings and performance#2683
simonmichael merged 26 commits into
mainfrom
holdings

Conversation

@simonmichael

@simonmichael simonmichael commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Building on the hledger 2.x lots support, here is our long-awaited holdings report - similar to the one in Beancount (and in all brokerage UIs). It is specified in doc/SPEC-holdings.md.

  • One row per holding account, or per lot with --lots; columns:
    Date, Age, Units, Unit/Avg cost, Cost, Price, Value, Weight,
    Gain (unrealised), Rgain (realised), XIRR.
  • Standard display modes and options: --tree/--depth/--drop/
    --no-elide, -S value sorting, -N, --round, pager, -o.
  • Valuation from P directives or --infer-market-prices, in each
    holding's cost commodity by default; -V/-X/--value supported.
  • Output formats: txt, csv, tsv, html (with per-column css classes and
    a sample hledger.css), fods, json.
  • Docs: command doc, manual lot-reporting integration, spec updates,
    roi-with-lots interop notes and comparison examples.
  • Also includes a lots fix (store the styled/widened cost basis) and
    26 new functional tests.

AI usage: Claude Fable 5, ~521k output tokens

lots-entries.journal and irr.journal contain notes on comparing the roi and holdings reports. (They seem to pretty much agree, when you run them just right.)

@simonmichael simonmichael added A-WISH Some kind of improvement request or proposal. investing Related to investments, lots, capital gains, etc. labels Aug 4, 2026
Comment thread doc/SPEC-holdings.md
Comment thread doc/SPEC-holdings.md Outdated
@simonmichael simonmichael added needs-testing To unblock: needs more developer testing or general usage and removed needs-testing To unblock: needs more developer testing or general usage labels Aug 5, 2026
…out mockup)

Adds a skeleton holdings command which shows a mockup of the planned
layout with sample data, rendered with the standard table machinery.
--lots and --tree select layout variants. Design notes and mockups
are in the new doc/SPEC-holdings.md.

AI usage: Claude Fable 5, ~42k output tokens
The holdings command now shows real data in list mode: the assets held
in lot-tracked accounts as of the report end date, one row per account,
or one row per lot with --lots. Columns: Date and Age (when the row's
lots share one date), Quantity, Unit/Avg cost, and Cost (total cost
basis), with a totals row (disable with -N). Amounts are displayed
normalised to their commodity's display precision. Query arguments are
supported; --tree is not yet.

Rows come from a single-period end-balances multiBalanceReport; per-lot
quantities are summed from lot subaccount postings, and each lot's cost
basis is parsed from its subaccount name. The command receives the
journal with lot detail uncollapsed regardless of --lots, aggregating
lots itself.

AI usage: Claude Fable 5, ~80k output tokens
Like print's --round, but defaulting to hard, since holdings is a
display report; --round=none/soft/all select the other rounding
strategies. An invalid value is reported before any output.

Until now, --round was supported only by some print-like commands; I
mistakenly thought it was needed here also. But it's working now and
might be useful, so let's keep it.

AI usage: Claude Fable 5, ~15k output tokens
Holdings are valued at the report date using the standard price oracle:
market prices come from P directives, and from transaction costs with
--infer-market-prices. Each holding is valued in its cost commodity when
possible, so Gain (absolute and percent) is meaningful; holdings with no
market price show blank Price, Value and Gain, as does the totals row
unless all rows are priced. The general -B/-V/-X/--value flags are
ignored; holdings does its own valuation.

AI usage: Claude Fable 5, ~30k output tokens
These select the valuation commodity and/or valuation date for the
Price and Value columns; the cost columns (Cost, Unit/Avg cost, and the
cost side of Gain) are also converted to the valuation commodity at the
valuation date, so percent gain is unaffected by currency conversion.
Converted amounts are normalised to the commodity's display precision,
like the rest of the report. Costs with no market price to the valuation
commodity are left unconverted, leaving Gain blank. --value=then is
rejected (holdings is a snapshot report), and -B/--cost has no effect.

Also expose amountValueAtDate from Hledger.Data.Valuation.

AI usage: Claude Fable 5, ~23k output tokens
AI usage: Claude Fable 5, ~6k output tokens
In tree mode, parent account rows aggregate the lots beneath them,
showing only lot-tracked commodities (not eg cash), with one line per
commodity in the Quantity and Price columns; --no-elide shows boring
parent accounts (and single lots) as separate rows. --depth clips and
aggregates the displayed rows as usual, while the lots beneath still
count. Totals are summed over the topmost displayed rows only, avoiding
double counting, and the totals row is omitted when there is a single
top-level row. Also, in list mode, rows whose lots all appear in a
deeper displayed row are no longer shown (previously a base account
posted to directly could duplicate its lot subaccount rows).

AI usage: Claude Fable 5, ~21k output tokens
Sorts rows by market value (or by cost, when unpriced), largest first.
In tree mode each level is sorted, keeping subtrees together.

AI usage: Claude Fable 5, ~12k output tokens
One record per displayed row and commodity, with machine-friendlier
fields: full account names, age in days, bare quantity and gain percent
numbers, and gain and gain percent as separate fields. Amounts have no
digit group marks; as in other commands' CSV output, the decimal mark
follows the commodity's display style. No totals records. (In tree
mode, parent account records repeat the data of their subaccounts.)

AI usage: Claude Fable 5, ~21k output tokens
Add a holdings example to the First lots example and Lot reporting
example sections, and a cross-reference in Lot subaccounts.

AI usage: Claude Fable 5, ~10k output tokens
An HTML table like the text table, but with single-line cells, rendered
with the standard spreadsheet-cell machinery. Amount cells are
right-aligned (headings are not). For styling, each cell has a css
class naming its column (account, date, age, quantity, unitcost, cost,
price, value, gain; totals row cells also have coltotal), and each
commodity amount is enclosed in a span with class "amount", eg
allowing wrapping within amounts to be prevented. Tree-mode account
names are indented with no-break spaces.

AI usage: Claude Fable 5, ~43k output tokens
It prevents wrapping within dates and commodity amounts, useful eg
for the holdings report. Kept at the top level for convenience, since
HTML reports look for hledger.css in the current directory.

AI usage: Claude Fable 5, ~5k output tokens
A flat opendocument spreadsheet readable by LibreOffice etc., with the
same single-line cells as the html output; both are now built from a
shared spreadsheet-cell table.

AI usage: Claude Fable 5, ~10k output tokens
A JSON array of holding objects, one per displayed row and commodity,
with the same fields as the CSV output: money amounts as machine-format
display strings, dates/ages/quantities/gain percents typed (quantities
and gain percents in hledger's usual JSON number encoding), and missing
values null. The csv/tsv/json outputs now all render from a shared
Holding record. Also add holdings to the manual's output format table.

AI usage: Claude Fable 5, ~17k output tokens
Weight is each row's percentage of the portfolio's total value (blank
unless all displayed holdings are priced in one commodity). Rgain is
the realised gain from disposals so far: each dispose posting's
proceeds minus the cost basis of the disposed units, for the lots at
or under the row's account. XIRR is the annualised internal rate of
return implied by the account's dated cashflows and current value,
calculated like roi's IRR (the solver setup is duplicated for now,
with cross-referencing comments); it includes realised gains.

The totals row is now always shown (unless -N), and computes Rgain and
XIRR at account level, from the displayed rows' base accounts: so they
include fully disposed lots, which have no row of their own (eg with
--lots). Fully disposed accounts still don't appear in the report, so
neither do their realised gains.

Ages are now shown compactly: in days or in years with one decimal
digit, eg 44d or 6.2y. The csv/tsv/json outputs gain weight, rgain and
xirr fields, keeping age numeric.

Also update examples/lots/lot-entries.journal so a plain `hledger
holdings` demonstrates all the columns: add a market price, and shift
the story dates one year into the past. Also explain the inferred
equity:unrealised-gain posting. And record two decisions in
SPEC-holdings.md: future-dated postings stay included by default (a
today-default report end date was tried and rolled back), and XIRR's
final cashflow is the displayed Value at the report date.

AI usage: Claude Fable 5, ~120k output tokens
AI usage: Claude Fable 5, ~5k output tokens
"Quantity" elsewhere in hledger means the number part of an amount,
while this column shows a full amount like "10 AAPL". Units avoids
the clash and matches fund statement terminology. Renamed consistently
in the display heading, the csv/tsv/json field names, the html css
class, code, docs and tests.

AI usage: Claude Fable 5, ~21k output tokens
- Document in Lot reporting > Recording gains and Commands > roi that on
  lots journals, roi's --pnl should match both generated gain accounts
  (eg --pnl 'revenues:gain|equity:unrealised-gain'), so that the
  unrealised-gain postings aren't counted as cash flows.

- Add examples/lots/irr.journal, a minimal roi vs holdings comparison,
  and append detailed comparison notes to it and lot-entries.journal.

- Roi.txt regenerated; also picks up some pending list reformatting.

AI usage: Claude Fable 5, ~30k output tokens
In the table outputs, the combined "$110 (+44.0%)" gain cell is now
two columns, UGain and UGain%, and Rgain is renamed to RGain.
The html css classes are now ugain, ugainpct, rgain, and for
consistency the csv/tsv/json field names gain and gainpct are renamed
to ugain and ugainpct.

Also regenerate the spec's layout mockups as real outputs showing all
current columns, and update the docs and examples.

AI usage: Claude Fable 5, ~35k output tokens
The Weight, UGain% and XIRR columns are now rendered as "%" commodity
amounts, so their precision, symbol placement and spacing can be
customised by declaring a display style for "%" - with a commodity
directive, with -c '0.00 %', or inferred from % amounts in the journal.
With no % style, a default is used: one decimal digit and the % sign
on the right with no space, as before. The csv/tsv/json outputs keep
bare percent numbers.

AI usage: Claude Fable 5, ~38k output tokens
AI usage: Claude Fable 5, ~5k output tokens
This groups the per-unit columns (Unit/Avg cost, Price) and the total
columns (Cost, Value), making the basis-vs-market comparisons easier
to scan. The csv/tsv/json field order follows suit.

AI usage: Claude Fable 5, ~10k output tokens
The report title ("Holdings on DATE") can now be customised with
--title=TEXT or suppressed with --title="", like other reports.

AI usage: Claude Fable 5, ~5k output tokens
@simonmichael simonmichael removed the needs-testing To unblock: needs more developer testing or general usage label Aug 6, 2026
@simonmichael
simonmichael merged commit 8840063 into main Aug 6, 2026
@simonmichael
simonmichael deleted the holdings branch August 6, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-WISH Some kind of improvement request or proposal. investing Related to investments, lots, capital gains, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants