Repository navigation
Lot tracking forces breaking the accounting equation / mis-valuing assets. #2731
Description
Activity
I recognize this is a really long issue description, and it ultimately covers several interrelated but separable behavior changes. It's currently one big issue because I think the overall picture is clearer when all the parts are in one place. I would hope to discuss here first, and then I'm happy to create sub-issues or separate issues for any parts that are agreed upon.
Thanks! I will study this.
some hledger docs have suggested accounting-equation-breakage is only a nuisance to be ignored
I hope they don't say it that strongly! I do think it could be a wasteful distraction for new users who just need basic accounting. But of course a person who wants to see it working and check it, should be able to. Related: https://hledger.org/balancing-the-accounting-equation.html
- addedinvestingRelated to investments, lots, capital gains, etc.Related to investments, lots, capital gains, etc.
on Sep 14, 2026 PS yes this is really huge. Any chance you could condense it ? TLDR ?
PPS I know it's a complex topic so I appreciate the in depth discussion and will read it. I guess the TLDR might be: "lot postings should participate in balancing at basis cost, not at transacted cost, and that this will fix the accounting equation; and moreover that balancing at transacted cost is the root of a lot of difficulties that have been showing up in the lot tracking implementation."
Reacted by Andrew PritchardI hope they don't say it that strongly! I do think it could be a wasteful distraction for new users who just need basic accounting. But of course a person who wants to see it working and check it, should be able to. Related: https://hledger.org/balancing-the-accounting-equation.html
It looks like the part I was remembering is https://hledger.org/1.52/hledger.html#equity-conversion-postings. I can't quite tell whether it's referring to the benign imbalance of
1 AAPL {$50} != $50(that one I agree is benign) or the real imbalance caused by pairs of differently-priced commodity postings. I assumed the former, since the example only shows a single transaction which would have only the benign form of the imbalance.PS yes this is really huge. Any chance you could condense it ? TLDR ?
PPS I know it's a complex topic so I appreciate the in depth discussion and will read it. I guess the TLDR might be: "lot postings should participate in balancing at basis cost, not at transacted cost, and that this will fix the accounting equation; and moreover that balancing at transacted cost is the root of a lot of difficulties that have been showing up in the lot tracking implementation."Yes, that's essentially the TLDR; there's another condensed form under the summary header which makes largely the same claim but without mentioning lots.
What I realized gradually while investigating/writing this is that it's not really about lots; it's about making sure sequences of priced commodity postings don't conjure/annihilate assets out of thin air, and lots just happen to a) encourage priced commodity postings and b) provide automation/checking to make commodity postings' basis more visible.
- …There is a problem with the entries above - they are not conventional Double Entry Bookkeeping (DEB) notation, and because of the "magical" transformation of one commodity into another, they cause an imbalance in the Accounting Equation
Sounds related: https://hledger.org/1.52/hledger.html?#equity-conversion-postings
Definitely related; roughly that says (combined with https://hledger.org/investments.html#recording-capital-gain) that priced commodity postings are the problem and conversion postings are the fix. My take is that there's a more nuanced answer on both sides:
First, priced commodity postings don't have to be a problem: if they're balanced at cost basis, then they force transactions to account gain coming from somewhere else, and no assets appear out of thin air, only change form between currency and equal-cost assets. Two things to note from that link (which I hadn't re-read in the last few months until now): one, the example
ledgertransaction is exactly the form I cited in the original issue description above and is balanced iff the priced commodity posting is balanced at cost; and two, the following paragraph recommends fiddling with the price annotation to makehledgerbalance the priced commodity posting at cost.Second, equity conversion postings on their own don't entirely solve the problem, they just change it from "money appeared" into "value accrued to equity bypassing revenue": if the conversion postings use the transacted price, then the value of gain comes out of equity:conversion leaving it with negative currency balance. If the conversion postings use the cost basis instead, then equity:conversion stays neutral (always contains positive and negative assets of equal cost) and any leftover must be attributed to gain/loss. (This also matches what the Tracking Investments link recommends in the context of equity postings).
It looks to me like both priced commodity postings and equity conversion postings can be considered valid (up to personal preference whether assets are equal to their currency cost), and in order to account gain correctly, both priced commodity postings and equity conversion postings need to use the cost basis rather than transacted price (for their currency aspect, i.e. for balancing priced commodity postings or for the currency side of the conversion postings).
Hi @awpr - I must admit I still haven't absorbed every word above in full detail, but I think I've understood the main points:
You're right that the accounting equation is unbalanced by disposals balancing at transacted cost.
I believe it's because we are not tracking the unrealised gain caused by market price changes; I just haven't implemented that
(related notes: https://github.com/hledgerorg/hledger/blob/main/doc/PLAN-ugain.md).
https://hledger.org/dev/hledger.html#gain-postings should mention the accounting equation imbalance, at least.What if transaction balancing used the basis cost as you propose ? This would be exactly how we do it in hledger 1.
Apparently these are two standard accounting conventions, both legitimate:
transacted-cost balancing (with revaluations) is "mark-to-market accounting", and basis-cost balancing is "historical cost accounting".Basis-cost balancing doesn't track the unrealised gain changes explicitly (though they can still be reported from market prices).
But it seems simpler and more intuitive.
I thought it would complicate transaction balancing, requiring full knowledge of prior lot history, but we already have that, so the implementation changes seem small.
For reparseability we just need to make sure the basis cost is made explicit.So I think you're right, and this should be changed. I'm looking into it.
Reacted by Arthur CinaderWell the changes weren't so small. But I think it's a net simplification, in addition to being more correct accounting. Actually it's a return to the lots/balancing design of February, now better understood and fleshed out.
A number of other improvements came with it. In particular, -B/--cost now shows basis cost when known, instead of transacted cost. This helps preserve
bse's zero total with -B, eg. -B is a very old flag, but the change only affects lots users. The old behaviour (showing transacted cost always) is now available as --value=transacted.Please test: PR #2744
- added a commit that references this issue
on Sep 23, 2026 This was merged into main. I believe it resolves the issue(s), if not we can reopen.
And, Thank you Andrew!
Thanks for the quick turnaround! I tried this out on the journals I had been using with a vibe-coded version and quickly ran into issues with acquires with fees; but this was my own journals' issue, as they weren't consistent with my own proposed balancing rules, and it was just being ignored because it wasn't checked on acquires. I had something like the following trying to record a notional price alongside a different cost basis, and it doesn't balance in the first at-transacted-cost pass:
2026-01-01 Buy assets:cash -$151.00 assets:stocks 1 AAPL {$151.00} @ $150.00I spent some time thinking about it and it seems like being able to capitalize fees as basis without adjusting the transacted price is a lot more involved1, so for now I'll need to change my CSV rules to omit or adjust the price and re-test; haven't gotten back to doing that yet. Will update when I get the chance, but at first glance the merged version looks sensible to me.
Footnotes
-
Blindly applying "balance at transacted cost without gain, then add gain according to basis" here leads to a capital loss of $1, which is sort of half correct, as it's not a realized loss but instead something that should offset a future gain. This looks like a case where I want the asset to sit on the books valued at $150 but with a cost basis of $151, which doesn't fit into the "basis = notional value on the books" model. ↩
-
Thanks for testing. I had to look up again how this works. Disposal transactions balance using cost basis now; but all other transactions still balance using transacted cost.
Also https://hledger.org/dev/hledger.html#cost-basis-vs-transacted-cost recommends keeping cost basis and transacted cost the same in acquisitions, and recording the difference another way (and we have an optional check for this). I'm not sure I understood your use case or the footnote here; also I haven't figured out a good workflow for capitalising acquisition fees (adjusting cost basis to include them). So I'm interested to hear what you think of this.
Actually, I'm having trouble now finding any valid case where basis and transacted cost should be recorded differently in an acquisition, and I'm starting to think we should not allow it.
Here are three of the cases mentioned in that doc, and the simple way of recording them that doesn't involve writing B != T:
commodity AAPL ; lots: 2026-01-01 gift of 10 AAPL from Mum, carryover basis $50/share equity:gifts received $-500 assets:stocks 10 AAPL @ $50 2026-02-01 inherited 10 AAPL, basis stepped up to market equity:inheritance $-1500 assets:stocks 10 AAPL @ $150 2026-03-01 10 RSUs vested at $150, taxed as income revenues:compensation $-1500 assets:stocks 10 AAPL @ $150I am convinced, and went ahead with this change. We now require @ and {} to be the same in an acquire, and the rewritten https://hledger.org/dev/hledger.html#acquire explains why.
Version:
hledger 1.99-g89eede297-20260418, linux-x86_64OS: Arch Linux 7.1.9-arch1-2
Docs: https://hledger.org/dev/hledger.html#first-lots-example
Example: to follow
Abstract
Lot tracking as currently implemented appears to force the accounting equation to be broken (if any gains or losses are realized). I recognize some hledger docs have suggested accounting-equation-breakage is only a nuisance to be ignored, and one should only look at
hledger bsand nothledger bse, but to me it means something is incorrect.I'll attempt to make the case that lot postings should participate in balancing at basis cost, not at transacted cost, and that this will fix the accounting equation; and moreover that balancing at transacted cost is the root of a lot of difficulties that have been showing up in the lot tracking implementation.
Argument
Running example
Let's use an augmented version of the first example in the lot tracking doc as a working example:
Demo output of
hledger -f test.journal bse -p monthly:Observe that the net should be zero in 2026-03-31 and 2026-04-30 because all revenues/expenses are swept into equity (
hledger incis truly 0 for all reporting periods). For 2026-03-31, "zero" means -$250, 5 AAPL{$50} (i.e. zero after valuing assets at cost), but instead it's $100 greater. At 2026-04-30, it should say literally 0, and instead it says $200.A different sort of trouble shows up with
hledger -f test.journal -B -p monthly:Now the assets and equity are balanced, but only because they're both wrong: in the last period, assets says we have net $500, but it should be $700; there's a phantom -$200 in the stocks account that's valuing nonexistent assets at negative cost. Something similar is happening at 2026-03-31, but it's less blatant because the real cost $250 plus the error -$100 is still positive.
Balancing at Basis
Here's my take on what went wrong here, first in the "no
-B" case.Using the discipline that commodities are considered equal to their transacted cost, the 2026-02-01 acquire transaction is balanced; the balance sheet is "balanced" afterwards, too: +10 AAPL @ $50 ~= $500. Nothing wrong yet.
The 2026-03-01 transaction is currently considered balanced by valuing the AAPL commodity at its transacted cost at sale, 5*$70=$350. When summing the two transactions, we get dAssets = -$150 + 5 AAPL@$50 ~= $100, dRevenues = -$100, dEquity = $100. The accounting equation here says dAssets + dRevenues + dEquity = 0, but instead it's $100. According to these books, if valuing assets at their transacted cost, $100 appeared out of thin air. This is a bit unclear after selling just half because we're trying to equate shares and dollars, but after selling the rest, it's crystal clear: assets are $700 and equity is only $500.
The lot tracking system attempts to account for the extra dEquity by using the
revenues:gainposting to record that the gain came in as revenue. But in order to make the first sale transaction "balanced", it also had to create an unrealised-gain posting. These two postings together effectively do nothing: revenues is just equity under a special name for "recent" negative equity changes, so the unrealised-gain posting offsets the gain posting, and there's no net change to equity+revenues to balance the change to assets. Observe that in the last reporting period, the $200 gain is correctly reported, but there's a spurious -$200 unrealised-gain that's making the net equity incorrect.Last time I tried using the lot tracking preview a few months ago, it was generating gain postings and giving them special treatment to be ignored in transaction balancing. This was actually better in terms of the accounting equation, but janky conceptually and in implementation, so I understand why it was changed. It was better in that disposals would have the correct net effect on equity+revenues, but at the cost of being forced to accept "unbalanced" transactions (both mentally and in implementation).
So, let's look closer at where the first $100 came from according to this ledger. If we consider commodities to be equal to their transacted price, then the $100 appeared in between the acquire and dispose, when the AAPL shares started being valued at $70. In some sense that's true, as the market price evidently changed between the two transactions; but it's not really compatible with the idea that accounts only change through transactions. Plus, even if we accept that the AAPL holdings increased through extra-transactional means, that increase should still have been balanced by equity+revenue changes, and in this ledger, it wasn't. The source of error here is that the USD value of AAPL as understood by transaction balancing changed when it was sold above basis cost, but the corresponding USD value in equity didn't.
So, what happens if we look at the second transaction as-written (without any gain postings) and try to balance it when valuing AAPL at basis cost instead? Then we have -$250 from the AAPL posting and +$350 from the assets posting. The difference is $100, exactly the gain. In order to be balanced under that methodology, the transaction needs a -$100 revenue posting:
How to read the above transaction in this "balance at basis cost" model: we sold 5 AAPL, which means converting it back to its corresponding 5 * $50 = $250 of basis; we also received $100 of gain as part of this transaction; and the total proceeds of $350 landed in
assets:cash. This is now balanced both locally (inside the transaction) and globally (in the accounting equation): dEquity is now $0 and the sum is 0 as it should be. The source of error in the previous ledger is gone, because the USD value of each share of AAPL in transaction balancing never changes from its cost basis.With the above balancing tweak, lot tracking still has the opportunity to infer a lot, and it seems to me it requires slightly less special-purpose code to do so. I think the following maximally-stripped-down version of the transaction has enough information to infer everything:
First, lot tracking can match the sale to a lot and find that the basis is $50. Then, gain inference can deduce a gain posting of -5 * ($70 - $50) = -$100. Finally, transaction balancing can deduce an amount of $350 for
assets:cashfrom the sum of the other two postings (note the AAPL posting acts as -$250). (Granted I don't know if the phases are in the right order here).Conveniently, this form of the transaction also works tolerably when balanced without lots under the old "balance at transacted cost" model (it'd still get the right $350 posting to
assets:cash, but would bring back the accounting equation problem).Balancing at Basis in Acquires
To stress-test the idea of valuing commodities at cost basis and inferring postings for the difference, let's see what happens when applying it to acquires, in situations where the basis of newly acquired lots would differ from their transacted cost. First up, option exercise:
This seems to check out; here the total gain posting should be $0, because disposing of the options without receiving USD is a "loss" (net shares * (cost basis - transacted cost) < 0), but receiving a stepped-up basis for the shares is a "gain" (net shares * (cost basis - transacted cost) > 0).
Next up, wash sales:
This also seems to check out: as long as the wash sale acquire posting gets the stepped-up basis, the gain posting derived from the difference between cost basis and transacted cost is exactly offsetting the loss that was disallowed.
Valuing at Cost
The
bse -Boutput for the running example was wrong too (it showed negative value in the stocks account); here's my best guess at what went wrong there.It looks almost like hledger is reinterpreting transactions in
-Bmode by replacing commodity postings with their transacted price. If that's the case, it explains what was observed: when we acquired 10 AAPL, that became +$500 into assets:stocks. When we sold 5 @ $70, that became -$350 from assets:stocks; and the same applies to the second sale. Net, that's -$200 to assets:stocks, which is the observed outcome.Intuitively I'd have expected
-Bto reinterpret balances after summing transactions in terms of the underlying commodities (although I see why this would be unachievable back when hledger didn't track cost basis). Under that behavior, we'd find that after the first sale we have 5 AAPL at $50 cost, valued at $250, and after the second sale we have 0 AAPL, valued at $0.If hledger were to treat asset postings as balancing at their cost basis instead of transacted cost, then this would correspond naturally to
-Btreating asset postings as having the cost basis as their amount, and this would give exactly the correct result: $250 value after the first sale, $0 after the second.Conversion Postings
Personally I prefer conversion postings over priced commodities, because they make the equity side actually reflect assets rather than costs. Luckily this seems to be reasonably compatible with the idea of permanently valuing commodities at their cost basis: instead of balancing commodities as equal to their cost basis, infer (or write)
equity:conversionpostings whose amount is derived from their cost basis.Under this model, instead of transmuting assets back to their USD cost basis at disposal, we just (partially, proportionally) undo the conversion postings from their acquisition. The role of lot tracking in this context would be to guarantee the correct USD amounts are assigned to
equity:conversionand torevenues:gain.In some sense the USD balance of
equity:conversionis the aggregate cost basis, in that it's exactly the amount that you're justified in "moving back to assets" on sale without treating it as revenue; without lot tracking, one could claim to take the full transacted price out ofequity:conversion, which would sort-of amount to trying to "use up" other shares' cost basis without selling them, and report less ($0) than the true gain. Lot tracking would be responsible for guaranteeing you take the correct amount of basis out ofequity:conversionfor a given disposal.The way I'd most prefer to see this turn out is that each lot gets its own (lot-named) conversion account, which at any given moment holds negative the remaining commodities in the lot and positive the remaining basis. No matter where these conversion accounts reside (assets, revenues, equity), the accounting equation is exactly upheld; only the reporting differs. If they're in assets, then net assets are reported as currency at cost basis (because the conversion postings undo the sale postings). If they're in revenues/expenses, then net assets include the commodities, equity remains in currency, and the difference is in the income statement. If they're in equity, then both equity and assets report the commodities.
Making them an additional top-level account hierarchy might also be a good option: then reporting on
conversionand sub-accounts tells you about cost basis per lot, the behavior of-Bcould be achieved by netting them with assets, and commodity-aware reporting could be achieved by netting them with equity.Without Lot Tracking
Most of what's written above applies even without automated lot tracking: we can have the same problematic journal with the same
hledger bse (-B)?results even without lot tracking (except without the net-zero pair of revenue:gain and equity:unrealized-gain postings). The same change of balancing priced commodity postings as their cost basis fixes the problem in the same way without lot tracking; it's just a lot more inconvenient and error-prone (because nothing infers or checks the basis price for you, and nothing infers or checks the relationship between gain amount and price/cost difference for you).In fact, my pre-lot-tracking CSV rules for a brokerage account effectively use the methodology of balancing priced commodities at their cost basis, just in the form of "conversion postings" instead of priced commodity postings: a sale has conversion postings based on the cost basis, asset postings based on the transacted price, and a gain posting for the difference. The main downsides are that it's less legible (I don't get to see the transacted price per share in the ledger) and less rigorously checked (no lot tracking).
Compatibility
Changing priced commodity postings to balance at cost seems to risk breaking some existing journals. A few issues to call out:
-B).Summary
It seems to me that priced commodity postings are broken, and have been for long before lot tracking existed; and that the reason they're broken is that the commodity postings should balance as equal to their cost basis, not their transacted cost. My opinion is that hledger 2 should do this, and I anticipate this would result in more-correct reporting across the board. Optionally inferring conversion postings derived from cost basis would be nice, too.