holdings: new command showing investment holdings and performance - #2683
Merged
Conversation
acinader
reviewed
Aug 4, 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
--lots; columns:Date, Age, Units, Unit/Avg cost, Cost, Price, Value, Weight,
Gain (unrealised), Rgain (realised), XIRR.
--tree/--depth/--drop/--no-elide,-Svalue sorting,-N,--round, pager,-o.--infer-market-prices, in eachholding's cost commodity by default;
-V/-X/--valuesupported.a sample hledger.css), fods, json.
roi-with-lots interop notes and comparison examples.
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.)