Dev notes related to better lot tracking in hledger.
Last updated: 2025-08-11
See also:
[[PTA lot tracking]]
## 2025-07
- Automatic lot subaccounts are enabled for certain commodities (eg, commodities tagged with lots:).
- Subaccounts named like `YYYYMMDD[SEQ] @ COST` (eg) are lot accounts.
- Non-print-like reports hide lot accounts by default. With a sufficient explicit depth (or `--lots` ?) they show lot accounts.
- `check accounts` ignores lot accounts.
- The date and the cost are required. They are the lot's original/nominal acquisition date and cost.
- `SEQ` should be used when there are same-day lots, to distinguish them and make them sort in acquisition order. It could be an integer (`-2-`), zero-padded integer (`-002-`), or a timestamp (`_13-59-03_`). It can also be used for noting extra information. It should not contain `@` or `:`.
- `COST` should be parseable as a hledger amount, with quantity and commodity symbol. The `@` sign is required, spaces are not.
```
account assets:exchange
account assets:exchange:usd
account assets:exchange:foo
account assets:wallet
account assets:wallet:foo
commodity USD
commodity FOO ; lots:
; An implicit entry and the equivalent explicit entry are shown for each transaction.
; (If testing this, use [not:]desc:explicit to select one or the other.)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 1. buy
2025-01-02 buy foo
assets:exchange:usd
assets:exchange:foo 10 FOO @ 2.00 USD
; equivalent to:
2025-01-02 buy foo
assets:exchange:usd
assets:exchange:foo:20250102 @ 2.00USD 10 FOO @ 2.00 USD
;
; need to know:
; the date
; any previous lots on this date (across all accounts), to determine SEQ
; the cost
; assets:exchange:foo:20250102 @ 2.00USD || 10 FOO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 2. buy more
2025-01-10 buy more foo
assets:exchange:usd
assets:exchange:foo 10 FOO @ 2.10 USD
; equivalent to:
2025-01-10 buy more foo
assets:exchange:usd
assets:exchange:foo:20250110 @ 2.10USD 10 FOO @ 2.10 USD
; assets:exchange:foo:20250102 @ 2.00USD || 10 FOO
; assets:exchange:foo:20250110 @ 2.10USD || 10 FOO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 3. move
2025-01-13 move some foo to wallet, FIFO
assets:exchange:foo -15 FOO
assets:wallet:foo 15 FOO
; equivalent to:
2025-01-13 move some foo to wallet, FIFO
assets:exchange:foo:20250102 @ 2.00USD -10 FOO @ 2.00 USD
assets:exchange:foo:20250110 @ 2.10USD -5 FOO @ 2.10 USD
assets:wallet:foo:20250102 @ 2.00USD 10 FOO @ 2.00 USD
assets:wallet:foo:20250110 @ 2.10USD 5 FOO @ 2.10 USD
;
; need to know:
; the FOO lots present in the source account(s)
; the FOO lots present in the destination account(s) (to avoid collisions) ?
; the disposal strategy
; restrictions:
; assets:exchange:foo:20250102 @ 2.00USD || 0
; assets:exchange:foo:20250110 @ 2.10USD || 5 FOO
; assets:wallet:foo:20250102 @ 2.00USD || 10 FOO
; assets:wallet:foo:20250110 @ 2.10USD || 5 FOO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 4. sell
2025-01-20 sell 12 FOO @ 2.30 USD, FIFO (from wallet, somehow)
assets:wallet:foo -12 FOO @ 2.30 USD
assets:exchange:usd 27.60 USD
; equivalent to:
2025-01-20 sell 12 FOO @ 2.30 USD, FIFO (from wallet, somehow) (explicit)
assets:wallet:foo:20250102 @ 2.00USD -10 FOO @ 2.00 USD
assets:wallet:foo:20250110 @ 2.10USD -2 FOO @ 2.10 USD
assets:exchange:usd 27.60 USD
revenues:gain -3.40 USD
; need to know:
; the FOO lots present in the source account(s)
; the amount received in the proceeds account
; the accounts to use for capital gain or capital loss
; the disposal strategy
; restrictions:
; assets:exchange:foo:20250102 @ 2.00USD || 0
; assets:exchange:foo:20250110 @ 2.10USD || 5 FOO
; assets:wallet:foo:20250102 @ 2.00USD || 0
; assets:wallet:foo:20250110 @ 2.10USD || 3 FOO
```
## 2025-02
Two imaginary notations, applied to the [[PTA lot tracking]] :
### Mockup: assisted subaccounts
Every lot is represented by a subaccount, but with some extra automation.
Lot accounts are indicated or detectable in some way,
and the lot's date and cost basis are encoded in the account name.\
Eg `assets:broker:AAA::2025-01-01_$1.10`\
or `assets:broker:AAA:{2025-01-01_$1.10}`.\
Lot accounts are omitted from reports by default, requiring the --lots flag to reveal them.
When there are many lots this would make reports more manageable without needing to carefully control account/report depth.
#### Acquire
```
2025-01-01
assets:broker:usd
assets:broker:aaa:{2025-01-01-1_$1.10} 10 AAA @ $1.10
2025-01-01
assets:broker:usd
assets:broker:aaa:{2025-01-01-2_$1.20} 10 AAA @ $1.20
2025-01-01
assets:broker:usd
assets:broker:aaa:{2025-01-01-3_$1.20} 10 AAA @ $1.20
```
#### Acquire custom
```
2025-02-01
assets:broker:usd
assets:broker:aaa:{2021-01-01_$0.40} 10 AAA @ $1.21
2025-02-02
assets:broker:usd
assets:broker:aaa:{2022-01-01_$0.40} 10 AAA @ $1.22
```
#### Show lots
%%
```
assets:broker:aaa::2021-01-01_$0.40 10 AAA
assets:broker:aaa::2022-01-01_$0.50 10 AAA
assets:broker:aaa::2025-01-01-1_$1.10 10 AAA
assets:broker:aaa::2025-01-01-2_$1.20 10 AAA
assets:broker:aaa::2025-01-01-3_$1.20 10 AAA
```
%%
```
assets:broker:aaa:{2021-01-01_$0.40} 10 AAA
assets:broker:aaa:{2022-01-01_$0.50} 10 AAA
assets:broker:aaa:{2025-01-01-1_$1.10} 10 AAA
assets:broker:aaa:{2025-01-01-2_$1.20} 10 AAA
assets:broker:aaa:{2025-01-01-3_$1.20} 10 AAA
```
#### Dispose FIFO
```
2025-03-01
assets:broker:aaa:{} -5 AAA @ $1.31
assets:broker:usd
2025-03-02
assets:broker:aaa:{} -24 AAA @ $1.32
assets:broker:usd
```
#### Dispose Spec ID
```
2025-03-03
assets:broker:aaa:{2022-01-01_$0.50} -5 AAA @ $1.33
assets:broker:aaa:{2025-01-01-2_$1.20} -5 AAA @ $1.33
assets:broker:usd
```
### Mockup: {...} after the amount
A pair of curly braces {...} following a posting's @ COST indicates a lot posting.
Lots could be enabled automatically for certain commodities or accounts, but an explicit notation simplifies things.
Accounts are allowed to have either lot postings or non-lot postings, not both.
If the posting amount is positive, it's a lot acquisition.
{} with nothing inside means use the defaults: the posting's date and transacted cost (@) become the lot's acquisition date and cost basis.
Or a custom date and/or cost basis can be specified inside the braces.
If the posting amount is negative, it's a lot disposal.
{} with lot selectors inside (date, perhaps intra-day sequence number, perhaps cost) specifies a single existing lot to be disposed of.
{} with nothing inside could mean select the default lot or multiple existing lots to dispose of.
#### Acquire
```
2025-01-01
assets:broker:usd
assets:broker:aaa 10 AAA @ $1.10 {}
2025-01-01
assets:broker:usd
assets:broker:aaa 10 AAA @ $1.20 {}
2025-01-01
assets:broker:usd
assets:broker:aaa 10 AAA @ $1.20 {}
```
#### Acquire custom
```
2025-02-01
assets:broker:usd
assets:broker:aaa 10 AAA @ $1.21 {2021-01-01 $0.40}
2025-02-02
assets:broker:usd
assets:broker:aaa 10 AAA @ $1.22 {2022-01-01 $0.50}
```
#### Show lots
```
assets:broker:aaa 10 AAA {2021-01-01 $0.40}
assets:broker:aaa 10 AAA {2022-01-01 $0.50}
assets:broker:aaa 10 AAA {2025-01-01-1 $1.10}
assets:broker:aaa 10 AAA {2025-01-01-2 $1.20}
assets:broker:aaa 10 AAA {2025-01-01-3 $1.20}
```
#### Dispose FIFO
```
2025-03-01
assets:broker:aaa -5 AAA @ $1.31 {} ; or: -5 AAA {} @ $1.31
assets:broker:usd
2025-03-02
assets:broker:aaa -24 AAA @ $1.32 {}
assets:broker:usd
```
#### Dispose Spec ID
```
2025-03-03
assets:broker:aaa -5 AAA @ $1.33 {2022-01-01}
assets:broker:aaa -24 AAA @ $1.33 {2025-01-01-2}
assets:broker:usd
```
## 2025-02
- One catch-all use case:\
An investor/trader of stocks or cryptocurrencies wants to keep track of their investment lots,
to accurately calculate gains/losses and to implement/check/document disposal policies like FIFO or Specific Identification.
They would like to do this on their own machine, for privacy, reliability, or to check the reports from their broker/exchange/investing app.
- Simplify goals, perhaps drastically, to make progress.
- Generally we don't want to see individual lots in reports by default, since there can be many of them.
- Notation comes first.
- Validity checks need not be enforced in the notation, they can be checked as a later step.
- Assume for now that lots can exist only in asset accounts.
- Assume that accounts (except aggregating parent accounts) contain lot postings or non-lot postings, but not both.
- Postings need to be able to specify lot attributes, to customise lots during acquisition or select them during disposals.
- The actual lots in an account are something else; they must be calculated by applying all postings and their lot specifications to that account's balance, in order, perhaps with validity checks.
- These actual lot details must be preserved and transferred whenever lots move, eg to a new account, owner, or data file.
This last is the biggest source of tedious, error-prone bookkeeping work with lots in PTA.
Whenever starting a new journal file, every lot must be declared as part of opening balances.
And if you are using the clopen method ([1], [2]), they must also be declared in closing balances.
There are tools to help with these.
But also, if you make any bookkeeping error and want to correct it later, that tends to force many updates to later journal entries.
Eg to all future closing/opening balances, to all future lot disposals in the same account,
and to any other of your accounts to which that account transferred lots (recursively).
(I'm not yet sure to what extent this verbosity can be safely avoided with automation.)
In the "real world", assuming the impact is relatively small, tolerating or adjusting for such errors is probably the norm.
[1]: https://joyful.com/hledger+workflow+2025#Journal+file+organising+principles
[2]: https://hledger.org/dev/hledger.html#close---clopen
## 2023
### Lot terminology
Some investment-related terminology, as we use it here and in the PTA world:
- "Investment" - something whose value fluctuates while you hold it.
- Acquiring, disposing - receiving and getting rid of investments, whether by purchase,
exchange, gift, inheritance, stock options...
- Augmenting, reducing - the same thing; terminology used in Beancount docs.
Most often the investment is an asset and acquiring/augmenting increases a positive balance,
but with other kinds of investments (options..) it might decrease a negative balance.
Acquiring/augmenting increases your exposure (risk), disposing/reducing reduces it.
- Lot - a quantity of an investment purchased at a specific time and cost.
It may also have descriptive note attached.
With many investments, lots must be tracked individually for tax reporting.
- Capital gain/loss - your net profit or loss arising from the change in value of an investment
since you acquired it. Some times abbreviated as "gains" in these docs.
While you are holding the investment, you have unrealised gains, which fluctuate along with the market value.
Once you dispose of it, you have realised gains.
Capital gain/loss has tax consequences.
- Cost basis - the nominal cost of a lot being held, used for calculating capital gains/losses. Can be adjusted up or down for various reasons while the lot is being held.
- Disposal method, reduction strategy, lot selection - the order in which lots are reduced, eg when you are selling a stock
or gifting some cryptocurrency, which ones do you reduce first ?
Common strategies: FIFO (first in first out), LIFO (last in first out),
and Specific Identification (a documented custom order).
The reduction strategy affects capital gains now and later, and has tax consequences.
Sometimes you can choose it, at other times it is mandated by the tax authorities.
### Lot ideas
2023-01 Some examples/brainstorming of lot notations and functionality.
I believe one could emulate most of ledger/beancount's lot tracking/selection with simpler syntax -
just @, with less or no need for {} (curly brace syntax).
#### Explicit lot accounts
Eg here, using explicit subaccounts to track lots, no {} is needed.:
```journal
2022-01-01 buy at 10
assets:aaa:20220101 10 AAA @ $10
assets:cash $-100
2022-02-01 buy at 12
assets:aaa:20220201 10 AAA @ $12
assets:cash $-120
2022-03-01 sell at 20
assets:aaa:20220101 -10 AAA @ $10 ; original cost basis
assets:aaa:20220201 -5 AAA @ $12
assets:cash $300
revenues:gains $-140
```
#### Inferring cost from lot account
Assuming each lot subaccount holds only one lot, the cost basis could be recalled automatically when selling, though it's less readable:
```journal
2022-01-01 buy at 10
assets:aaa:20220101 10 AAA @ $10
assets:cash $-100
2022-02-01 buy at 12
assets:aaa:20220201 10 AAA @ $12
assets:cash $-120
2022-03-01 sell at 20
assets:aaa:20220101 -10 AAA ; @ $10 implied
assets:aaa:20220201 -5 AAA ; @ $12 implied
assets:cash $300
revenues:gains $-140
```
#### Cost in lot account name
Cost basis could also be indicated in the subaccount name:
```journal
2022-01-01 buy at 10
assets:aaa:20220101_$10 10 AAA @ $10
assets:cash $-100
2022-02-01 buy at 12
assets:aaa:20220201_$12 10 AAA @ $12
assets:cash $-120
2022-03-01 sell at 20
assets:aaa:20220101_$10 -10 AAA ; @ $10 implied, now more clear
assets:aaa:20220201_$12 -5 AAA
assets:cash $300
revenues:gains $-140
```
#### Automatic lot accounts
Lot subaccounts could be created automatically, without having to write them; and could be used to select lots when withdrawing:
```journal
2022-01-01 buy at 10
assets:aaa 10 AAA @ $10 ; creates _20220101_$10 subaccount
assets:cash $-100
2022-02-01 buy at 12
assets:aaa 10 AAA @ $12 ; creates _20220201_$12
assets:cash $-120
2022-03-01 sell at 20
assets:aaa:20220201_$12 -10 AAA ; select lot by subaccount
assets:aaa:20220101_$10 -5 AAA ; LIFO order here
assets:cash $300
revenues:gains $-130
```
#### Implicit lots
Or there could be no lot subaccounts, just lots tracked implicitly by the tool, with special commands to view them, as in ledger/beancount:
```journal
2022-01-01 buy at 10
assets:aaa 10 AAA @ $10 ; creates an implicit lot
assets:cash $-100
2022-02-01 buy at 12
assets:aaa 10 AAA @ $12 ; view lots with bal --lots
assets:cash $-120
```
#### Reduction strategy
Whether explicit, automatic or implicit, lots could be selected automatically according to some reduction strategy,
specified eg with a tag:
```journal
2022-03-01 sell at 20, FIFO
assets:aaa -15 AAA ; reduce lots FIFO by default
assets:cash $300
revenue:gains ; $-140 calculated
```
```journal
2022-03-01 sell at 20, LIFO
assets:aaa -15 AAA ; reduce:LIFO
assets:cash $300
revenue:gains ; $-130 calculated
```
The above are easy to enter but less informative and hard to calculate by eye; you could use the tool to convert to a more explicit entry:
```journal
2022-03-01 sell at 20, FIFO
assets:aaa -10 AAA @ $10
assets:aaa -5 AAA @ $12
assets:cash $300
revenue:gains $-140
```
```journal
2022-03-01 sell at 20, LIFO
assets:aaa -10 AAA @ $12
assets:aaa -5 AAA @ $10
assets:cash $300
revenue:gains $-130
```
#### Lot selection syntax
If lots are implicit, ie there are no subaccounts by which to select them,
some special syntax is needed to allow identifying them individually by cost, date, and/or note.
This could be {}, [], tags, or something new. Eg:
```journal
2022-03-01 sell at 20, taking 3 alternately from each lot
assets:aaa -3 AAA {@ $10} ; lot 1
assets:aaa -3 AAA {2022-02-01} ; lot 2
assets:aaa -3 AAA {buy at 10} ; lot 1
assets:aaa -3 AAA {@ $10, 2022-02-01, buy at 12} ; lot 2
assets:aaa -3 AAA ; lot-date:2022-01-01, lot-cost:$10, lot-note:buy at 10, (lot 1)
assets:cash $300
revenue:gains $-138
```
#### Use of curly braces
I don't see the need to use {} as much as Ledger/Beancount do.
In particular, Ledger/Beancount's {} syntax allows creating a lot with a cost basis
different from what it cost you in the transaction acquiring it.
What is the real need for this, and how often is it needed ?
It's not needed eg when buying a commodity at a rate different from the market rate; you can do:
```journal
2022-01-01 receive AAA, currently worth $10, with effective cost to us of ~$11 because of fees
revenues:usd -10 AAA @ $10
expenses:fees 1 AAA
equity:basis adjustment -1 AAA
assets:cash 9 AAA @ $11.111
commodity $0.00 ; help hledger balance the above
```
#### Investments vs one-time transactions
Not yet mentioned: some commodities/balances fluctuate in value while
you hold them (eg an investment) and others are a one-time conversion
(eg buying foreign currency at the airport).
@ can be used for both of these, it's essentially a matter of which cost you calculate with when disposing:
```journal
2022-01-01 buy at 10, hold with fluctuating value
assets:aaa 10 AAA @ $10 ; today's acquisition cost
assets:cash $-100
2022-03-01 sell at 20, with capital gain/loss
assets:aaa -10 AAA @ $10 ; original acquisition cost
assets:cash $200
revenue:gains $-100
```
```journal
2022-01-01 exchange SEK for USD, one-time conversion
assets:cash -100 SEK
assets:cash 10 USD @ 10 SEK ; today's conversion cost
2022-03-01 exchange back to SEK, one-time conversion
assets:cash -10 USD @ 11 SEK ; today's conversion cost
assets:cash 110 SEK
```
I believe @ and {} were intended to/can/do distinguish between these.
If using only @ there needs to be some other mechanism to indicate fluctuating value vs one-time conversion, or so it seems -
eg an annotation on the transaction, the account, or the commodity.
### Cost and price syntax
#### In Ledger and hledger
- In the journal, a `P DATE COMMODITY AMOUNT` directive some commodity's market price in some other commodity on DATE.
(A timestamp may be added, but is ignored.)
- In a posting, `AMT @ UNITCOST` declares the per-unit cost that was used to convert AMT into the cost's commodity.
Eg: `2A @ 3B` records that 2A was posted, in exchange for 6B.
- `@@ TOTALCOST` is another form of `@`, sometimes more convenient.
Eg: `2A @@ 5.99B` records that 2A was posted in exchange for 5.99B.
#### In Ledger
- `@ UNITCOST`
Any use of `@` also generates an implicit `P` directive.
Eg:
2019/1/1
a 2A @ 3B
b
in the journal is equivalent to writing
2019/1/1
a 2A @ 3B
b
P 2019/1/1 A 1.5B
- `{UNITCOST}`
- `{=FIXEDUNITCOST}`
The following are variants of the above; they work the same way except
that you write the total instead of the unit cost:
- `@@ TOTALCOST`
- `{{TOTALCOST}}`
- `{{=FIXEDTOTALCOST}}`
#### In hledger
- `@` does not generate a market price by default; with `--infer-market-prices` it does.
- `{}` and `{=}` are ignored
## 2020
### Capital gains
#### A model for capital gains
Capital gain/loss (when the value of assets you hold increases/decreases
due to market price fluctuations) - is an important topic, since it can
generate tax liability.
Here is a description of how it works, intended for both users and
builders of accounting software (especially, plain text accounting
software). (I'm a software engineer, not an accountant. In places there
may be better accounting terms I'm not familiar with yet.)
- lots/units -
A quantity of some commodity, acquired at a certain cost on a certain date,
is called a *lot*, or *unit*. (I'm not sure which is the most standard term. Using lot for now.)
- Since you might have purchased the lot on a stock exchange, received it as a gift,
or something else, we'll call this event *lot acquisition*, on the *acquisition date*.
- Later you might sell the lot for cash, or exchange it for something else, or gift it.
We'll call this *lot disposal*.
- You might have paid current market value for the lot, or you might have
paid less or more than that. We'll call what you paid/exchanged the *acquisition amount*.
- I think the acquisition amount is also called the *basis* or *cost basis*.
Or possibly the current market value is the basis, regardless of what you paid.
Perhaps it depends. To be clarified. The basis at which you acquired a lot is important.
- After acquisition, while you are still holding the lot, if the market value of that commodity goes up (or down),
your potential return from disposing of the lot increases (or decreases).
This is known as *capital gain (or loss)* (we'll just call it "capital gain").
At this stage, the gain is only "on paper", so it is called *unrealised capital gain*.
This is not usually considered revenue, or taxable.
- It's common to be holding multiple lots, perhaps many, even in a single account.
Eg, say you buy a small amount of some stock or cryptocurrency each week.
Each purchase adds a new lot to your assets. We'll call this a *multi-lot balance*, or *balance*.
- Unrealised capital gains are calculated for a lot at a certain point in time.
Likewise for a multi-lot balance.
- realised capital gain...
- lot withdrawal strategies...
- specific identification...
#### Capital gains in hledger
- postings can have multiple commodities and multiple costs; each of
these parts is a deposit or withdrawal to the account
-
```haskell
-- | Given a list of amounts all in the same commodity, interprets them
-- as a sequence of lot deposits (the positive amounts) and withdrawals
-- (the negative amounts), and applies them in order using the FIFO
-- strategy for withdrawals, then returns the resulting lot balance (as
-- another, shorter, list of amounts).
sumLots :: [Amount] -> [Amount]
```