Notes about investment lot tracking in [hledger](https://hledger.org) and other [plain text accounting](https://plaintextaccounting.org) apps.\ Last updated: 2025-08-13 Discussion: [mastodon](https://fosstodon.org/@simonmic/113970313986816653), [PTA forum](https://forum.plaintextaccounting.org/t/lot-tracking-in-pta-joyful-systems/492), [hledger chat](https://matrix.hledger.org/)... See also: [[hledger lot tracking]] ## Introduction Tracking investment lots is often necessary to calculate capital gains accurately and to comply with tax law. It's a tricky, increasingly widespread bookkeeping need. This page aims to be a useful guide to the topic, for PTA users and PTA developers. Here are gathered related links and FAQs; then a sort of test suite of common tasks involving lots; then reproducible examples of solving these with each of the lot tracking methods available in PTA. Finally, there are some dev notes on design and implementation of lot tracking in PTA. (How best to do this in a robust way, is a big question that's hard to answer.) General links: - https://en.wikipedia.org/wiki/Cost_basis - https://www.costbasis.com - https://www.irs.gov/forms-pubs/about-publication-550 Investment Income and Expenses - https://www.irs.gov/forms-pubs/about-publication-551 Basis of Assets Which non-PTA apps or sites do this job well ? - Most mature accounting apps which advertise investments support, probably. Eg [GnuCash](https://wiki.gnucash.org/wiki/Lots_GUI), [2](https://wiki.gnucash.org/wiki/Concept_of_Lots). - Stock brokers and cryptocurrency exchanges provide detailed lot reports. - Online and offline apps built for this job, often aimed at cryptocurrency users. Eg [BittyTax](https://github.com/BittyTax/BittyTax). What solutions are there in the PTA world ? - hledger users currently do it [manually with subaccounts](https://hledger.org/investments.html#what-about-lots-). This method can be used in any double entry system. It is intuitive but also verbose and tedious, requiring advanced text-editing skills. - Ledger has [lot notation](https://hledger.org/dev/hledger.html#other-costlot-notations) which helps automate this. [2](https://www.d10.dev/blog/ledger-cli-trade/) - Beancount has [simpler lot notation](https://hledger.org/dev/hledger.html#other-costlot-notations) which helps automate this to a greater extent, and with more validation. Also: - The [Lotter](https://src.d10.dev/lotter/doc/tip/README.md) addon tool adds limited automated lot tracking to Ledger or hledger. It does not support custom lot dates, moving lots between accounts, etc. - The [hledger-lots](https://hledger.org/scripts.html#hledger-lots) tool adds limited automated lot tracking to hledger. It does not support custom lot dates, moving lots between accounts, etc. - The [hledger-move](https://hledger.org/scripts.html#hledger-move) tool helps generate journal entries for the manual subaccounts method (as does the [hledger close](https://hledger.org/dev/hledger.html#close) command). PTA links: - https://hledger.org/dev/hledger.html#other-costlot-notations - https://github.com/simonmichael/hledger/issues/1015 overview of related PTA docs and hledger issues - [2015 hledger discussion](https://groups.google.com/g/hledger/c/e8Ss7ZL4ADI) beginning of hledger's costs/investment support, & a description of Beancount's from Martin B - https://plaintextaccounting.org/Investing-and-trading - https://hledger.org/cookbook.html#investing-and-trading - [Full-fledged hledger: Manual lot tracking](https://github.com/adept/full-fledged-hledger/wiki/Manual-lot-tracking) discussion of UK disposal rules What is the scope of disposal methods like FIFO - per account, per broker, per commodity ? It's unclear. No obvious IRS guidance found. LLMs disagree. Discussion suggests the IRS will consider it per commodity, across all brokers and accounts (as it is in the wash sale rule). But in practice brokers' cost basis reports are specific to them and even to each account held with them. Accounting software may ideally need to support different scopes. Assume per account for now. ## Tests Here are a few tasks/scenarios we'd like a lot tracking system to handle, as conveniently as possible. The following market prices can be assumed, if needed for context: ``` P 2025-01-01 AAA $1.10 ; morning P 2025-01-01 AAA $1.20 ; afternoon P 2025-02-01 AAA $1.21 P 2025-02-02 AAA $1.22 P 2025-03-01 AAA $1.31 P 2025-03-02 AAA $1.32 P 2025-03-03 AAA $1.33 ``` ### Acquire lots The first task is to record the acquisition of some lots. To make things more challenging, some have the same date and/or cost. ``` 2025-01-01 buy 10 AAA at 1.10 2025-01-01 buy 10 AAA at 1.20 (same day) 2025-01-01 buy 10 AAA at 1.20 (same day and cost) ``` ### Acquire lots with custom date/cost basis Next is to record the acquisition of lots with a custom prior acquisition date and cost basis, as when receiving lots as a gift. ``` 2025-02-01 receive 10 AAA acquired on 2021-01-01 at $0.40 2025-02-02 receive 10 AAA acquired on 2022-01-01 at $0.50 ``` ### Show lots Then show the lots currently held. They should be: ``` 10 AAA @ $0.40 acquired 2021-01-01 10 AAA @ $0.50 acquired 2022-01-01 10 AAA @ $1.10 acquired 2025-01-01 (1) 10 AAA @ $1.20 acquired 2025-01-01 (2) 10 AAA @ $1.20 acquired 2025-01-01 (3) ``` ### Dispose of lots, FIFO Then dispose of some lots in FIFO order. First 5 AAA (should come from the 2021 lot).\ Then 24 AAA (should come from the 2021, 2022, and 2025 (1) lots). ### Dispose of lots, Spec ID Then withdraw from two specifically identified lot(s):\ a total of 10 AAA from the 2022 lot and the 2025 (2) lot. *(TODO: adjust this to run in sequence after the test above)* ### Move lots Move a whole or fractional number of lots to a new account or broker, by each of the disposal methods. This combines Dispose and Acquire, and could provide additional automation. ### Enforce or check disposal method Specify and implement/enforce disposal methods, or allow them to be checked as a separate step. ### Assert lots Assert that given lots exist in an account (like balance assertions). ### Show lots 2 Show the remaining lots. At this point they should be: ``` 1 AAA @ $1.20 acquired 2025-01-01 (2) 10 AAA @ $1.20 acquired 2025-01-01 (3) ``` ### Show average cost Show the lots' average cost basis, both unweighted and weighted. ### Show unrealised gains Show current unrealised gains/losses, per lot. ### Show realised gains Record and show the gains/losses realised as part of a lot disposal (per lot ?) ### Show transactions Show all the transactions in explicit form, if possible. ### Transition to a new file Begin a new journal file, bringing forward all lots and balances from the previous period, and possibly closing the balances in the old file. An important task for some, eg when data gets large. ## PTA app examples Here are examples / results of implementing the above Tests in various PTA apps. Make your browser wide enough, or font small enough, so that the examples don't line-wrap. Some of the journal entries below may be incomplete (eg lacking real world details like fees). ### Manual subaccounts Using manual subaccounts to track lots. This method works in hledger, Ledger, Beancount, or any double entry system. #### 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 revenues:gifts assets:broker:aaa:2021-01-01_$0.40 10 AAA @ $0.40 2025-02-02 revenues:gifts assets:broker:aaa:2022-01-01_$0.50 10 AAA @ $0.50 ``` #### Show lots ``` $ hledger bal aaa -YE Balance changes in 2025: || 2025 ======================================++======== 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 --------------------------------------++-------- || 50 AAA ``` #### Dispose FIFO ``` 2025-03-01 assets:broker:aaa:2021-01-01_$0.40 -5 AAA @ $0.40 assets:broker:usd $6.55 revenues:gains 2025-03-02 assets:broker:aaa:2021-01-01_$0.40 -5 AAA @ $0.40 assets:broker:aaa:2022-01-01_$0.50 -10 AAA @ $0.50 assets:broker:aaa:2025-01-01-1_$1.10 -9 AAA @ $1.10 assets:broker:usd $31.68 revenues:gains ``` #### Dispose Spec ID ``` 2025-03-03 assets:broker:aaa:2025-01-01-1_$1.10 -1 AAA @ $1.10 assets:broker:aaa:2025-01-01-2_$1.20 -9 AAA @ $1.20 assets:broker:usd $13.30 revenues:gains ``` #### Show lots 2 ``` $ hledger bal aaa -YE Balance changes in 2025: || 2025 ======================================++======== assets:broker:aaa:2021-01-01_$0.40 || 0 assets:broker:aaa:2022-01-01_$0.50 || 0 assets:broker:aaa:2025-01-01-1_$1.10 || 0 assets:broker:aaa:2025-01-01-2_$1.20 || 1 AAA assets:broker:aaa:2025-01-01-3_$1.20 || 10 AAA --------------------------------------++-------- || 11 AAA ``` The above also works with Ledger: ``` $ ledger bal aaa -E --flat 0 assets:broker:aaa:2021-01-01_$0.40 0 assets:broker:aaa:2022-01-01_$0.50 0 assets:broker:aaa:2025-01-01-1_$1.10 1 AAA assets:broker:aaa:2025-01-01-2_$1.20 10 AAA assets:broker:aaa:2025-01-01-3_$1.20 -------------------- 11 AAA ``` And here it is in Beancount: ```beancount option "operating_currency" "USD" 2025-01-01 open Assets:Broker:Aaa:2021-01-01-0-40 2025-01-01 open Assets:Broker:Aaa:2022-01-01-0-50 2025-01-01 open Assets:Broker:Aaa:2025-01-01-1-1-10 2025-01-01 open Assets:Broker:Aaa:2025-01-01-2-1-20 2025-01-01 open Assets:Broker:Aaa:2025-01-01-3-1-20 2025-01-01 open Assets:Broker:Usd 2025-01-01 open Income:Gains 2025-01-01 open Income:Gifts 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa:2025-01-01-1-1-10 10 AAA @ 1.10 USD 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa:2025-01-01-2-1-20 10 AAA @ 1.20 USD 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa:2025-01-01-3-1-20 10 AAA @ 1.20 USD 2025-02-01 * Income:Gifts Assets:Broker:Aaa:2021-01-01-0-40 10 AAA @ 0.40 USD 2025-02-02 * Income:Gifts Assets:Broker:Aaa:2022-01-01-0-50 10 AAA @ 0.50 USD 2025-03-01 * Assets:Broker:Aaa:2021-01-01-0-40 -5 AAA @ 0.40 USD Assets:Broker:Usd 6.55 USD Income:Gains 2025-03-02 * Assets:Broker:Aaa:2021-01-01-0-40 -5 AAA @ 0.40 USD Assets:Broker:Aaa:2022-01-01-0-50 -10 AAA @ 0.50 USD Assets:Broker:Aaa:2025-01-01-1-1-10 -9 AAA @ 1.10 USD Assets:Broker:Usd 31.68 USD Income:Gains 2025-03-03 * Assets:Broker:Aaa:2025-01-01-1-1-10 -1 AAA @ 1.10 USD Assets:Broker:Aaa:2025-01-01-2-1-20 -9 AAA @ 1.20 USD Assets:Broker:Usd 13.30 USD Income:Gains ``` ``` $ bean-query subaccounts.beancount "SELECT account, units(sum(position)), cost(sum(position)) WHERE account ~ 'Aaa' GROUP BY 1 ORDER BY account;" account units( cost(s ----------------------------------- ------ ------ Assets:Broker:Aaa:2021-01-01-0-40 Assets:Broker:Aaa:2022-01-01-0-50 Assets:Broker:Aaa:2025-01-01-1-1-10 Assets:Broker:Aaa:2025-01-01-2-1-20 1 AAA 1 AAA Assets:Broker:Aaa:2025-01-01-3-1-20 10 AAA 10 AAA ``` #### Show realised gains ``` $ hledger reg gains --invert 2025-03-01 revenues:gains $4.55 $4.55 2025-03-02 revenues:gains $14.78 $19.33 2025-03-03 revenues:gains $1.40 $20.73 ``` ``` $ ledger reg gains --invert 25-Mar-01 <Unspecifie.. revenues:gains $4.55 $4.55 25-Mar-02 <Unspecifie.. revenues:gains $14.78 $19.33 25-Mar-03 <Unspecifie.. revenues:gains $1.40 $20.73 ``` #### Show transactions ``` $ hledger print -x 2025-01-01 assets:broker:usd $-11.00 assets:broker:aaa:2025-01-01-1_$1.10 10 AAA @ $1.10 2025-01-01 assets:broker:usd $-12.00 assets:broker:aaa:2025-01-01-2_$1.20 10 AAA @ $1.20 2025-01-01 assets:broker:usd $-12.00 assets:broker:aaa:2025-01-01-3_$1.20 10 AAA @ $1.20 2025-02-01 revenues:gifts $-4.00 assets:broker:aaa:2021-01-01_$0.40 10 AAA @ $0.40 2025-02-02 revenues:gifts $-5.00 assets:broker:aaa:2022-01-01_$0.50 10 AAA @ $0.50 2025-03-01 assets:broker:aaa:2021-01-01_$0.40 -5 AAA @ $0.40 assets:broker:usd $6.55 revenues:gains $-4.55 2025-03-02 assets:broker:aaa:2021-01-01_$0.40 -5 AAA @ $0.40 assets:broker:aaa:2022-01-01_$0.50 -10 AAA @ $0.50 assets:broker:aaa:2025-01-01-1_$1.10 -9 AAA @ $1.10 assets:broker:usd $31.68 revenues:gains $-14.78 2025-03-03 assets:broker:aaa:2025-01-01-1_$1.10 -1 AAA @ $1.10 assets:broker:aaa:2025-01-01-2_$1.20 -9 AAA @ $1.20 assets:broker:usd $13.30 revenues:gains $-1.40 ``` #### Transition to a new file hledger's `close` command helps with this. You copy the opening balances transaction into the new file, and (if using [clopen method](#clopen+method)) the closing balances transaction to the old file. ``` $ hledger close --clopen --show-costs -e 2026 2025-12-31 closing balances ; clopen: assets:broker:aaa:2025-01-01-2_$1.20 -1 AAA @ $1.20 = 0 AAA assets:broker:aaa:2025-01-01-3_$1.20 -10 AAA @ $1.20 = 0 AAA assets:broker:usd $-16.53 = $0.00 equity:opening/closing balances 2026-01-01 opening balances ; clopen: assets:broker:aaa:2025-01-01-2_$1.20 1 AAA @ $1.20 = 1 AAA assets:broker:aaa:2025-01-01-3_$1.20 10 AAA @ $1.20 = 10 AAA assets:broker:usd $16.53 = $16.53 equity:opening/closing balances ``` As a matter of style you might want to add the `@ COST`s to the opening balances transaction's balance assertions, which must be done manually for now. hledger ignores costs there, but they make the situation clearer. ``` 2026-01-01 opening balances ; clopen: assets:broker:aaa:2025-01-01-2_$1.20 1 AAA @ $1.20 = 1 AAA @ $1.20 assets:broker:aaa:2025-01-01-3_$1.20 10 AAA @ $1.20 = 10 AAA @ $1.20 assets:broker:usd $16.53 = $16.53 equity:opening/closing balances ``` ### Ledger lot notation #### Acquire `@ COST` creates lots: ``` 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 ``` ``` $ ledger bal --lots --flat -E --no-total aaa 10 AAA {$1.1} [2025/01/01] (1) 10 AAA {$1.2} [2025/01/01] (2) 10 AAA {$1.2} [2025/01/01] (3) assets:broker:aaa ``` But it doesn't allow adding a custom `[LOT DATE]` or `(LOT NOTE)`. So it's better to use the Beancount-like `{COST}` syntax, which does: ``` 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA {$1.10} (1) 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA {$1.20} (2) 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA {$1.20} (3) ``` Although this adds whitespace to the report (a display bug): ``` $ ledger -f ledger.ledger bal --lots --flat -E --no-total aaa 10 AAA {$1.1} (1) 10 AAA {$1.2} (2) 10 AAA {$1.2} (3) assets:broker:aaa ``` #### Acquire custom ``` 2025-02-01 revenues:gifts assets:broker:aaa 10 AAA {$0.40} [2021-01-01] 2025-02-02 revenues:gifts assets:broker:aaa 10 AAA {$0.50} [2022-01-01] ``` #### Show lots ``` $ ledger -f ledger.ledger bal --lots --flat -E --no-total aaa 10 AAA {$0.4} [2021/01/01] 10 AAA {$0.5} [2022/01/01] 10 AAA {$1.1} (1) 10 AAA {$1.2} (2) 10 AAA {$1.2} (3) assets:broker:aaa ``` #### Dispose FIFO Ledger doesn't apply FIFO automatically; you must specify the lots manually. And if you added a custom lot date or lot note when acquiring, you must also specify those when disposing. Ledger won't report an error if you dispose of a non-existent lot. ``` 2025-03-01 assets:broker:aaa -5 AAA {$0.40} [2021-01-01] assets:broker:usd $6.55 revenues:gains 2025-03-02 assets:broker:aaa -5 AAA {$0.40} [2021-01-01] assets:broker:aaa -10 AAA {$0.50} [2022-01-01] assets:broker:aaa -9 AAA {$1.10} (1) assets:broker:usd $31.68 revenues:gains ``` #### Dispose Spec ID ``` 2025-03-03 assets:broker:aaa -1 AAA {$1.10} (1) assets:broker:aaa -9 AAA {$1.20} (2) assets:broker:usd $13.30 revenues:gains ``` #### Show lots 2 ``` $ ledger -f ledger.ledger bal --lots --flat -E --no-total aaa 1 AAA {$1.20} (2) 10 AAA {$1.20} (3) assets:broker:aaa ``` #### Show transactions ``` $ ledger print 2025/01/01 <Unspecified payee> assets:broker:usd assets:broker:aaa 10 AAA {$1.10} (1) 2025/01/01 <Unspecified payee> assets:broker:usd assets:broker:aaa 10 AAA {$1.20} (2) 2025/01/01 <Unspecified payee> assets:broker:usd assets:broker:aaa 10 AAA {$1.20} (3) 2025/02/01 <Unspecified payee> revenues:gifts assets:broker:aaa 10 AAA {$0.40} [2021/01/01] 2025/02/02 <Unspecified payee> revenues:gifts assets:broker:aaa 10 AAA {$0.50} [2022/01/01] 2025/03/01 <Unspecified payee> assets:broker:aaa -5 AAA {$0.40} [2021/01/01] assets:broker:usd $6.55 revenues:gains 2025/03/02 <Unspecified payee> assets:broker:aaa -5 AAA {$0.40} [2021/01/01] assets:broker:aaa -10 AAA {$0.50} [2022/01/01] assets:broker:aaa -9 AAA {$1.10} (1) assets:broker:usd $31.68 revenues:gains 2025/03/03 <Unspecified payee> assets:broker:aaa -1 AAA {$1.10} (1) assets:broker:aaa -9 AAA {$1.20} (2) assets:broker:usd $13.30 revenues:gains ``` #### Transition to a new file ? ### Beancount lot notation #### Acquire ``` option "operating_currency" "USD" 2025-01-01 open Assets:Broker:Aaa AAA "FIFO" 2025-01-01 open Assets:Broker:Usd 2025-01-01 open Income:Gains 2025-01-01 open Income:Gifts 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa 10 AAA {1.10 USD} 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa 10 AAA {1.20 USD} 2025-01-01 * Assets:Broker:Usd Assets:Broker:Aaa 10 AAA {1.20 USD} ``` #### Acquire custom ``` 2025-02-01 * Income:Gifts Assets:Broker:Aaa 10 AAA {0.40 USD, 2021-01-01} 2025-02-02 * Income:Gifts Assets:Broker:Aaa 10 AAA {0.50 USD, 2022-01-01} ``` #### Show lots ``` $ bean-query beancount.beancount "SELECT account, units(sum(position)) as units, cost_number as cost, cost_date as acquisition_date WHERE account ~ 'Aaa' GROUP BY account, cost_date, currency, cost_currency, cost_number, account_sortkey(account) ORDER BY account_sortkey(account), currency, cost_date" account units cost acquisitio ----------------- ------ ---- ---------- Assets:Broker:Aaa 10 AAA 0.40 2021-01-01 Assets:Broker:Aaa 10 AAA 0.50 2022-01-01 Assets:Broker:Aaa 10 AAA 1.10 2025-01-01 Assets:Broker:Aaa 20 AAA 1.20 2025-01-01 ``` More detail: ``` $ bean-query beancount.beancount "SELECT account, units(sum(position)) as units, cost_number as cost, first(getprice(currency, cost_currency)) as price, cost(sum(position)) as book_value, value(sum(position)) as market_value, safediv((abs(sum(number(value(position)))) - abs(sum(number(cost(position))))), sum(number(cost(position)))) * 100 as unrealized_profit_pct, cost_date as acquisition_date WHERE account ~ 'Aaa' GROUP BY account, cost_date, currency, cost_currency, cost_number, account_sortkey(account) ORDER BY account_sortkey(account), currency, cost_date" account units cost p book_valu market unrealized_profit_pct acquisitio ----------------- ------ ---- - --------- ------ ------------------------------- ---------- Assets:Broker:Aaa 10 AAA 0.40 4.00 USD 10 AAA 150.0 2021-01-01 Assets:Broker:Aaa 10 AAA 0.50 5.00 USD 10 AAA 100 2022-01-01 Assets:Broker:Aaa 10 AAA 1.10 11.00 USD 10 AAA -9.090909090909090909090909091 2025-01-01 Assets:Broker:Aaa 20 AAA 1.20 24.00 USD 20 AAA -16.66666666666666666666666667 2025-01-01 ``` #### Dispose FIFO With the "FIFO" declaration for the Assets:Broker:Aaa account, above, you can write just `{}` to dispose in FIFO order. This is very convenient. "LIFO" and "STRICT" orders are also supported. ``` 2025-03-01 * Assets:Broker:Aaa -5 AAA {} Assets:Broker:Usd 6.55 USD Income:Gains 2025-03-02 * Assets:Broker:Aaa -24 AAA {} Assets:Broker:Usd 31.68 USD Income:Gains ``` #### Dispose Spec ID ``` 2025-03-03 * Assets:Broker:Aaa -1 AAA {1.10 USD} Assets:Broker:Aaa -9 AAA {1.20 USD} Assets:Broker:Usd 13.30 USD Income:Gains ``` #### Show lots 2 ``` $ bean-query beancount.beancount "SELECT account, units(sum(position)) as units, cost_number as cost, cost_date as acquisition_date WHERE account ~ 'Aaa' GROUP BY account, cost_date, currency, cost_currency, cost_number, account_sortkey(account) ORDER BY account_sortkey(account), currency, cost_date" account units cost acquisitio ----------------- ------ ---- ---------- Assets:Broker:Aaa 0.40 2021-01-01 Assets:Broker:Aaa 0.50 2022-01-01 Assets:Broker:Aaa 1.10 2025-01-01 Assets:Broker:Aaa 11 AAA 1.20 2025-01-01 ``` More detail: ``` $ bean-query beancount.beancount "SELECT account, units(sum(position)) as units, cost_number as cost, first(getprice(currency, cost_currency)) as price, cost(sum(position)) as book_value, value(sum(position)) as market_value, safediv((abs(sum(number(value(position)))) - abs(sum(number(cost(position))))), sum(number(cost(position)))) * 100 as unrealized_profit_pct, cost_date as acquisition_date WHERE account ~ 'Aaa' GROUP BY account, cost_date, currency, cost_currency, cost_number, account_sortkey(account) ORDER BY account_sortkey(account), currency, cost_date" account units cost p book_valu market unrealized_profit_pct acquisitio ----------------- ------ ---- - --------- ------ ------------------------------ ---------- Assets:Broker:Aaa 0.40 0 2021-01-01 Assets:Broker:Aaa 0.50 0 2022-01-01 Assets:Broker:Aaa 1.10 0 2025-01-01 Assets:Broker:Aaa 11 AAA 1.20 13.20 USD 11 AAA -16.66666666666666666666666667 2025-01-01 ``` #### Show transactions ? #### Transition to a new file ? ### Lotter Install: ``` $ fossil clone https://src.d10.dev/lotter && cd lotter && go install ``` Lotter reads a subset of Ledger/hledger journal syntax. Commodity symbols must be text and on the right. Costs should be written with `@ COST`, not `{COST}`. Generate lot postings (extra postings which update special accounts representing lots): ``` $ lotter -f a.journal > a.lotter.journal ``` The entries it generates have wrong signs to my eyes; you can use `--invert` to flip them. Lots purchased on the same day at the same cost will be combined. #### Acquire ``` 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.10 USD 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.20 USD 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.20 USD ``` Generates: ``` 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA ; @ 1.10 USD [Lot::2025/01/01:[email protected]] -10 AAA ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 11 USD ; :BUY: (basis) 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA ; @ 1.20 USD [Lot::2025/01/01:[email protected]] -10 AAA ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 12 USD ; :BUY: (basis) 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA ; @ 1.20 USD [Lot::2025/01/01:[email protected]] -10 AAA ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 12 USD ; :BUY: (basis) ``` #### Acquire custom Custom lot dates, or differing lot cost and transaction cost, are not supported. ``` 2025-02-01 revenues:gifts assets:broker:aaa 10 AAA @ 0.40 USD 2025-02-02 revenues:gifts assets:broker:aaa 10 AAA @ 0.50 USD ``` Generates: ``` 2025-02-01 revenues:gifts assets:broker:aaa 10 AAA ; @ 0.40 USD [Lot::2025/02/01:[email protected]] -10 AAA ; :BUY: (inventory) [Lot::2025/02/01:[email protected]] 4 USD ; :BUY: (basis) 2025-02-02 revenues:gifts assets:broker:aaa 10 AAA ; @ 0.50 USD [Lot::2025/02/02:[email protected]] -10 AAA ; :BUY: (inventory) [Lot::2025/02/02:[email protected]] 5 USD ; :BUY: (basis) ``` #### Show lots ``` $ hledger -f lotter.lotter bal -YE --invert Lot cur:AAA Balance changes in 2025: || 2025 ==============================++======== Lot::2025/01/01:[email protected] || 10 AAA Lot::2025/01/01:[email protected] || 20 AAA Lot::2025/02/01:[email protected] || 10 AAA Lot::2025/02/02:[email protected] || 10 AAA ------------------------------++-------- || 50 AAA ``` #### Dispose FIFO The cost of lots being disposed must be specified. The lack of support for custom lot dates means you can't implement FIFO in that case. Eg here, FIFO would require disposing of the 0.40 lot with the 2021 acquisition date first. ``` 2025-03-01 assets:broker:aaa -5 AAA @ 0.40 USD assets:broker:usd 6.55 USD revenues:gains 2025-03-02 assets:broker:aaa -5 AAA @ 0.40 USD assets:broker:aaa -10 AAA @ 0.50 USD assets:broker:aaa -9 AAA @ 1.10 USD assets:broker:usd 31.68 USD revenues:gains ``` Generates: ``` 2025-03-01 assets:broker:aaa -5 AAA ; @ 0.40 USD assets:broker:usd 6.55 USD revenues:gains [Lot::2025/01/01:[email protected]] 5 AAA ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -5.5 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] 3.5 USD ; :GAIN:SHORTTERM: (loss) 2025-03-02 assets:broker:aaa -5 AAA ; @ 0.40 USD assets:broker:aaa -10 AAA ; @ 0.50 USD assets:broker:aaa -9 AAA ; @ 1.10 USD assets:broker:usd 31.68 USD revenues:gains [Lot::2025/01/01:[email protected]] 5 AAA ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -5.5 USD ; :SELL: (basis consumed) [Lot::2025/01/01:[email protected]] 10 AAA ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -12 USD ; :SELL: (basis consumed) [Lot::2025/01/01:[email protected]] 9 AAA ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -10.8 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] 11.4 USD ; :GAIN:SHORTTERM: (loss) ``` #### Dispose Spec ID This too is not really supported in general. ``` 2025-03-03 assets:broker:aaa -1 AAA @ 1.10 USD assets:broker:aaa -9 AAA @ 1.20 USD assets:broker:usd 13.30 USD revenues:gains ``` Generates: ``` 2025-03-03 assets:broker:aaa -1 AAA ; @ 1.10 USD assets:broker:aaa -9 AAA ; @ 1.20 USD assets:broker:usd 13.30 USD revenues:gains [Lot::2025/01/01:[email protected]] 1 AAA ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -1.2 USD ; :SELL: (basis consumed) [Lot::2025/02/01:[email protected]] 9 AAA ; :SELL: (inventory consumed) [Lot::2025/02/01:[email protected]] -3.6 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] -7.1 USD ; :GAIN:SHORTTERM: (gain) ``` #### Show lots 2 The quantity of lots remaining is right but their cost bases are wrong: ``` $ hledger -f lotter.lotter bal -YE --invert Lot cur:AAA Balance changes in 2025: || 2025 ==============================++======== Lot::2025/01/01:[email protected] || 0 Lot::2025/01/01:[email protected] || 0 Lot::2025/02/01:[email protected] || 1 AAA Lot::2025/02/02:[email protected] || 10 AAA ------------------------------++-------- || 11 AAA ``` #### Show realised gains Lotter adds extra currency noise that isn't very helpful: ``` $ hledger reg gains --invert 2025-03-01 revenues:gains -5 AAA -5 AAA 6.55 USD 6.55 USD 2025-03-02 revenues:gains -24 AAA -29 AAA 31.68 USD 38.23 USD 2025-03-03 revenues:gains -10 AAA -39 AAA 13.30 USD 51.53 USD ``` And the gains are wrong, the total should be 20.73 not 51.53: ``` $ hledger reg gains --invert cur:USD 2025-03-01 revenues:gains 6.55 USD 6.55 USD 2025-03-02 revenues:gains 31.68 USD 38.23 USD 2025-03-03 revenues:gains 13.30 USD 51.53 USD ``` #### Show transactions ``` $ hledger print -x 2025-01-01 assets:broker:usd -10 AAA assets:broker:aaa 10 AAA ; @ 1.10 USD [Lot::2025/01/01:[email protected]] -10 AAA @@ 11 USD ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 11 USD ; :BUY: (basis) 2025-01-01 assets:broker:usd -10 AAA assets:broker:aaa 10 AAA ; @ 1.20 USD [Lot::2025/01/01:[email protected]] -10 AAA @@ 12 USD ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 12 USD ; :BUY: (basis) 2025-01-01 assets:broker:usd -10 AAA assets:broker:aaa 10 AAA ; @ 1.20 USD [Lot::2025/01/01:[email protected]] -10 AAA @@ 12 USD ; :BUY: (inventory) [Lot::2025/01/01:[email protected]] 12 USD ; :BUY: (basis) 2025-02-01 revenues:gifts -10 AAA assets:broker:aaa 10 AAA ; @ 0.40 USD [Lot::2025/02/01:[email protected]] -10 AAA @@ 4 USD ; :BUY: (inventory) [Lot::2025/02/01:[email protected]] 4 USD ; :BUY: (basis) 2025-02-02 revenues:gifts -10 AAA assets:broker:aaa 10 AAA ; @ 0.50 USD [Lot::2025/02/02:[email protected]] -10 AAA @@ 5 USD ; :BUY: (inventory) [Lot::2025/02/02:[email protected]] 5 USD ; :BUY: (basis) 2025-03-01 assets:broker:aaa -5 AAA ; @ 0.40 USD assets:broker:usd 6.55 USD revenues:gains 5 AAA revenues:gains -6.55 USD [Lot::2025/01/01:[email protected]] 5 AAA @@ 2.0 USD ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -5.5 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] 3.5 USD ; :GAIN:SHORTTERM: (loss) 2025-03-02 assets:broker:aaa -5 AAA ; @ 0.40 USD assets:broker:aaa -10 AAA ; @ 0.50 USD assets:broker:aaa -9 AAA ; @ 1.10 USD assets:broker:usd 31.68 USD revenues:gains 24 AAA revenues:gains -31.68 USD [Lot::2025/01/01:[email protected]] 5 AAA @ 0.70 USD ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -5.5 USD ; :SELL: (basis consumed) [Lot::2025/01/01:[email protected]] 10 AAA @ 0.70 USD ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -12 USD ; :SELL: (basis consumed) [Lot::2025/01/01:[email protected]] 9 AAA @ 0.70 USD ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -10.8 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] 11.4 USD ; :GAIN:SHORTTERM: (loss) 2025-03-03 assets:broker:aaa -1 AAA ; @ 1.10 USD assets:broker:aaa -9 AAA ; @ 1.20 USD assets:broker:usd 13.30 USD revenues:gains 10 AAA revenues:gains -13.30 USD [Lot::2025/01/01:[email protected]] 1 AAA @ 1.19 USD ; :SELL: (inventory consumed) [Lot::2025/01/01:[email protected]] -1.2 USD ; :SELL: (basis consumed) [Lot::2025/02/01:[email protected]] 9 AAA @ 1.19 USD ; :SELL: (inventory consumed) [Lot::2025/02/01:[email protected]] -3.6 USD ; :SELL: (basis consumed) [Lot:Income:short term gain] -7.1 USD ; :GAIN:SHORTTERM: (gain) ``` ### hledger-lots hledger-lots provides: - Interactive `buy` and `sell` commands that help add valid lot acquisition and disposal entries to your journal. - A `list` command that analyses the journal and prints the current lots. This expects the disposal entries (at least) to be in the format generated by `sell`; if you have created some yourself, they will probably break `list`. Install:\ Current hledger-lots (1.4.2) requires an older hledger version (>=1.18 && <=1.32) in PATH. ``` $ pip install -U hledger-lots ``` The journal file needs to contain metadata like this: ``` #+hledger-lots avg_cost:false, check:true #+hledger-lots no_desc: ``` Non-text currency symbols are supported, but in generated entries they will be on the right. #### Acquire The `buy` command can help generate entries like these, though they are simple. ``` 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.10 USD 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.20 USD 2025-01-01 assets:broker:usd assets:broker:aaa 10 AAA @ 1.20 USD ``` #### Acquire custom Like Lotter, hledger-lots doesn't support custom lot dates, or differing lot and transaction costs. ``` 2025-02-01 revenues:gifts assets:broker:aaa 10 AAA @ 0.40 USD 2025-02-02 revenues:gifts assets:broker:aaa 10 AAA @ 0.50 USD ``` #### Show lots The list command lists lots. Here it aggregates them for some reason: ``` $ hledger-lots view AAA date price base_cur qtty acct ---------- ------- ---------- ------ ----------------- 2025-01-01 1.1000 USD 10 assets:broker:aaa 2025-01-01 1.2000 USD 10 assets:broker:aaa 2025-01-01 1.2000 USD 10 assets:broker:aaa 2025-02-01 0.4000 USD 10 assets:broker:aaa 2025-02-02 0.5000 USD 10 assets:broker:aaa Info ---- Commodity: AAA Quantity: 50 Amount: 44.00 Average Cost: 0.8800 Market Data not available ``` #### Dispose FIFO Entries generated with the `sell` command. The lack of custom date support means they are not correct FIFO. ``` 2025-03-01 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:5.00, price:1.31 ; avg_cost:1.1000, xirr:1.85% annual percent rate 30/360US assets:broker:usd 6.55 USD assets:broker:aaa -5.0 AAA @ 1.1 USD ; buy_date:2025-01-01, base_cur:USD revenues:gains -1.05 USD 2025-03-02 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:24.00, price:1.32 ; avg_cost:1.1792, xirr:0.95% annual percent rate 30/360US assets:broker:usd 31.68 USD assets:broker:aaa -5 AAA @ 1.1 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -10 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -9.0 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD revenues:gains -3.38 USD ``` #### Dispose Spec ID The `sell` command doesn't support specific identification of lots. Here's the third disposal, but using the default (pseudo) FIFO: ``` 2025-03-03 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:10.00, price:1.33 ; avg_cost:0.4800, xirr:6500.49% annual percent rate 30/360US assets:broker:usd 13.30 USD assets:broker:aaa -1 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -9.0 AAA @ 0.4 USD ; buy_date:2025-02-01, base_cur:USD revenues:gains -8.50 USD ``` #### Show lots 2 ``` $ hledger-lots view AAA date price base_cur qtty acct ---------- ------- ---------- ------ ----------------- 2025-01-01 1.1000 USD 0 assets:broker:aaa 2025-01-01 1.2000 USD 0 assets:broker:aaa 2025-01-01 1.2000 USD 0 assets:broker:aaa 2025-02-01 0.4000 USD 1 assets:broker:aaa 2025-02-02 0.5000 USD 10 assets:broker:aaa Info ---- Commodity: AAA Quantity: 11 Amount: 5.40 Average Cost: 0.4909 Market Data not available ``` #### Show realised gains If the test scenario was supported, the total should be 20.73 not 12.93. ``` $ hledger -f hledger-lots.hledger reg --invert -w80 gains 2025-03-01 Sold AAA revenues:gains 1.05 USD 1.05 USD 2025-03-02 Sold AAA revenues:gains 3.38 USD 4.43 USD 2025-03-03 Sold AAA revenues:gains 8.50 USD 12.93 USD ``` #### Show transactions ``` $ hledger print -x 2025-01-01 assets:broker:usd -11.00 USD assets:broker:aaa 10 AAA @ 1.10 USD 2025-01-01 assets:broker:usd -12.00 USD assets:broker:aaa 10 AAA @ 1.20 USD 2025-01-01 assets:broker:usd -12.00 USD assets:broker:aaa 10 AAA @ 1.20 USD 2025-02-01 revenues:gifts -4.00 USD assets:broker:aaa 10 AAA @ 0.40 USD 2025-02-02 revenues:gifts -5.00 USD assets:broker:aaa 10 AAA @ 0.50 USD 2025-03-01 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:5.00, price:1.31 ; avg_cost:1.1000, xirr:1.85% annual percent rate 30/360US assets:broker:usd 6.55 USD assets:broker:aaa -5.0 AAA @ 1.1 USD ; buy_date:2025-01-01, base_cur:USD revenues:gains -1.05 USD 2025-03-02 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:24.00, price:1.32 ; avg_cost:1.1792, xirr:0.95% annual percent rate 30/360US assets:broker:usd 31.68 USD assets:broker:aaa -5 AAA @ 1.1 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -10 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -9.0 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD revenues:gains -3.38 USD 2025-03-03 Sold AAA ; cost_method:fifo ; commodity:AAA, qtty:10.00, price:1.33 ; avg_cost:0.4800, xirr:6500.49% annual percent rate 30/360US assets:broker:usd 13.30 USD assets:broker:aaa -1 AAA @ 1.2 USD ; buy_date:2025-01-01, base_cur:USD assets:broker:aaa -9.0 AAA @ 0.4 USD ; buy_date:2025-02-01, base_cur:USD revenues:gains -8.50 USD ```