Beancount's inventory system is a powerful feature for tracking assets that are bought and sold over time, such as stocks, mutual funds, or foreign currencies. It allows for precise tracking of cost basis, which is essential for calculating capital gains and understanding portfolio performance. This tutorial covers the core mechanics of managing inventories in your ledger.
Core Concepts
At its heart, inventory management revolves around tracking positions. A "position" is simply an amount of a commodity held in an account. Beancount distinguishes between two fundamental types of positions.
Position Types
-
Simple Position (No Cost): This is a standard balance posting. It represents an amount of a commodity without any associated acquisition cost. It's suitable for cash or simple balance assertions.
Assets:Bank:Checking 100.00 USD -
Position with Cost Basis: This type of position includes not only the number of units and the commodity but also the cost at which it was acquired. This is the foundation of inventory tracking. The cost is specified within curly braces
{}.Assets:Invest:VTSAX 10 VTSAX {100.00 USD, "lot-1"}In this example, we hold 10 units of
VTSAX. Each unit was acquired at a cost of $100.00 USD. This specific batch of shares is identified as a "lot."
Inventory Operations
There are two primary operations you can perform on an inventory:
-
Augmentations (Adding to inventory): When you buy a commodity, you augment your inventory. You create a new lot with a specific number of units and a cost basis.
2024-01-15 * "Buy shares" Assets:Invest:STOCK 50 STOCK {25.00 USD, "lot-1"} Assets:Bank:Checking -1250.00 USDHere, we buy 50 units of
STOCKat a per-unit cost of $25.00 USD. This creates a lot in theAssets:Invest:STOCKaccount. -
Reductions (Removing from inventory): When you sell a commodity, you reduce your inventory. You must specify which lot you are selling from. This is done by providing matching information in the curly braces.
2024-01-20 * "Sell shares" Assets:Invest:STOCK -25 STOCK {25.00 USD} Assets:Bank:Checking 625.00 USDIn this transaction, we are selling 25 units of
STOCKfrom the lot that was purchased at $25.00 USD per unit.
Booking Methods
When you reduce an inventory, Beancount needs a rule to decide which specific lot to pull from if several lots match the reduction. This rule is called the "booking method." You can set a default for the whole file with an option, or give one account its own method on the open directive.
Beancount 3.2.3 accepts seven method names: STRICT (the default), STRICT_WITH_SIZE, NONE, FIFO, LIFO, HIFO and AVERAGE. Six of them are implemented; AVERAGE parses but raises an error the moment it has to book a reduction, as the AVERAGE section below shows.
1. STRICT (Default)
The STRICT method is the default and the safest booking method. It enforces explicit and unambiguous matching.
2024-01-01 open Assets:Invest:STOCK "STRICT"- Requires Exact Lot Match: The reduction posting's cost specifier (
{...}) must identify a single lot — by cost, by acquisition date, by label, or by any combination of them. - Errors on Ambiguous Matches: If the specifier matches more than one lot, Beancount raises an
AmbiguousMatchErrorinstead of guessing. - Exception: If a reduction removes exactly the total number of units the specifier matches, an empty specifier (
{}) is allowed, and the reduction is split across those lots.
This ledger holds two lots and sells one of them by naming its cost, which is unambiguous:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Strict STK "STRICT"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy the first lot"
Assets:Broker:Strict 10 STK {100.00 USD}
Assets:Broker:Cash -1000.00 USD
2024-02-10 * "Buy the second lot"
Assets:Broker:Strict 10 STK {120.00 USD}
Assets:Broker:Cash -1200.00 USD
; The cost identifies exactly one lot, so STRICT is satisfied.
2024-06-01 * "Sell the $120.00 lot"
Assets:Broker:Strict -10 STK {120.00 USD} @ 150.00 USD
Assets:Broker:Cash 1500.00 USD
Income:GainsIt loads without errors, books $300.00 of gain to Income:Gains, and leaves 10 STK {100.00 USD} in the account.
Replace that last posting with an empty specifier and the same file fails:
; Rejected under STRICT: "-10 STK {}" matches both lots.
2024-06-01 * "Sell 10 shares"
Assets:Broker:Strict -10 STK {} @ 150.00 USD
Assets:Broker:Cash 1500.00 USD
Income:GainsBeancount reports Ambiguous matches for "-10 STK {}" and lists the candidates. Selling the whole position is fine, though, because there is nothing left to choose between:
; Allowed under STRICT: -20 STK is the entire holding, so the empty
; specifier is split across both lots.
2024-06-01 * "Close the position"
Assets:Broker:Strict -20 STK {} @ 150.00 USD
Assets:Broker:Cash 3000.00 USD
Income:GainsThat books $800.00 of gain — $3,000.00 of proceeds against $1,000.00 + $1,200.00 of basis — and leaves the account empty. This is a property of STRICT itself, not something you have to switch to STRICT_WITH_SIZE for.
2. FIFO (First-In, First-Out)
The FIFO method automatically books reductions against the oldest available lots first.
2024-01-01 open Assets:Invest:STOCK "FIFO"- Automatic Resolution: It resolves ambiguity by selecting the oldest matching lots.
- Chronological Matching: You assume you are selling the assets you have held the longest. Several tax authorities treat this as the default when you have not identified a lot.
3. LIFO (Last-In, First-Out)
The LIFO method is the opposite of FIFO. It books reductions against the newest available lots first.
2024-01-01 open Assets:Invest:STOCK "LIFO"- Reverse Chronological Order: It selects the most recently acquired matching lots.
- Newest, not most expensive: LIFO picks by acquisition date only. It happens to sell the highest-cost shares when prices have been rising, but if your newest lot is your cheapest — which is what the example below is built to show — LIFO will realize the largest gain, not the smallest. The method that always sells the most expensive shares is
HIFO, described next.
4. HIFO (Highest-In, First-Out)
The HIFO method books reductions against the most expensive available lots first, whatever their date.
2024-01-01 open Assets:Invest:STOCK "HIFO"- Cost-Ranked Matching: It selects the matching lots with the highest cost basis.
- Smallest Realized Gain: For a given sale price, selling the highest-cost shares realizes the smallest gain (or the largest loss). Whether you may use it is a jurisdiction question — in the United States, for example, choosing a lot at all requires specific identification at the time of sale — so treat the method as a bookkeeping mechanism and confirm the tax election separately.
5. Comparing FIFO, LIFO and HIFO on the same lots
The three methods only differ when the oldest, the newest and the most expensive lot are three different lots. This ledger arranges exactly that — lot A is the oldest, lot C is the newest, and the middle lot B is the most expensive — and then sells 10 shares out of three accounts that differ only in their booking method:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Fifo STK "FIFO"
1970-01-01 open Assets:Broker:Lifo STK "LIFO"
1970-01-01 open Assets:Broker:Hifo STK "HIFO"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
; Lot A - the oldest, at $100.00 per share
2024-01-10 * "Buy lot A"
Assets:Broker:Fifo 10 STK {100.00 USD}
Assets:Broker:Lifo 10 STK {100.00 USD}
Assets:Broker:Hifo 10 STK {100.00 USD}
Assets:Broker:Cash -3000.00 USD
; Lot B - the most expensive, at $120.00 per share
2024-02-10 * "Buy lot B"
Assets:Broker:Fifo 10 STK {120.00 USD}
Assets:Broker:Lifo 10 STK {120.00 USD}
Assets:Broker:Hifo 10 STK {120.00 USD}
Assets:Broker:Cash -3600.00 USD
; Lot C - the newest, at $90.00 per share
2024-03-10 * "Buy lot C"
Assets:Broker:Fifo 10 STK {90.00 USD}
Assets:Broker:Lifo 10 STK {90.00 USD}
Assets:Broker:Hifo 10 STK {90.00 USD}
Assets:Broker:Cash -2700.00 USD
; Sell 10 shares out of each account at $150.00 and let each
; account's booking method choose which lot leaves.
2024-06-01 * "Sell 10 shares from each account"
Assets:Broker:Fifo -10 STK {} @ 150.00 USD
Assets:Broker:Lifo -10 STK {} @ 150.00 USD
Assets:Broker:Hifo -10 STK {} @ 150.00 USD
Assets:Broker:Cash 4500.00 USD
Income:GainsIt loads with zero errors and books $1,400.00 of gain in total, split like this:
| Account | Method | Lot booked | Cost basis | Realized gain | Lots remaining |
|---|---|---|---|---|---|
Assets:Broker:Fifo | FIFO | lot A, 2024-01-10 | $100.00 | $500.00 | 10 @ $120.00, 10 @ $90.00 |
Assets:Broker:Lifo | LIFO | lot C, 2024-03-10 | $90.00 | $600.00 | 10 @ $100.00, 10 @ $120.00 |
Assets:Broker:Hifo | HIFO | lot B, 2024-02-10 | $120.00 | $300.00 | 10 @ $100.00, 10 @ $90.00 |
The LIFO row is the one worth staring at: it realized the biggest gain of the three, because the newest lot was also the cheapest.
6. STRICT_WITH_SIZE
STRICT_WITH_SIZE is STRICT plus one extra tie-breaker: when several lots match but exactly one of them holds precisely the number of units you are removing, that lot is chosen.
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Sized STK "STRICT_WITH_SIZE"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy 10 shares"
Assets:Broker:Sized 10 STK {100.00 USD}
Assets:Broker:Cash -1000.00 USD
2024-02-10 * "Buy 7 shares"
Assets:Broker:Sized 7 STK {120.00 USD}
Assets:Broker:Cash -840.00 USD
; Only one lot holds exactly 7 units, so the empty specifier resolves.
2024-06-01 * "Sell 7 shares"
Assets:Broker:Sized -7 STK {} @ 150.00 USD
Assets:Broker:Cash 1050.00 USD
Income:GainsThat books $210.00 of gain against the $120.00 lot. The identical file with "STRICT" on the open line fails with Ambiguous matches for "-7 STK {}".
7. AVERAGE (accepted, but not implemented)
AVERAGE is a valid name — option "booking_method" "AVERAGE" and open … "AVERAGE" both parse — but Beancount 3.2.3 has no implementation behind it. Everything here loads until the sale:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Avg STK "AVERAGE"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy 10 shares at $10.00"
Assets:Broker:Avg 10 STK {10.00 USD}
Assets:Broker:Cash -100.00 USD
2024-02-10 * "Buy 10 more at $8.00"
Assets:Broker:Avg 10 STK {8.00 USD}
Assets:Broker:Cash -80.00 USD
; An average-cost engine would book this at $9.00 per share. This one refuses.
2024-06-01 * "Sell 5 shares"
Assets:Broker:Avg -5 STK {}
Assets:Broker:Cash 45.00 USD
Income:GainsThe moment that reduction has to be booked, the loader stops with:
AVERAGE method is not supportedDo not plan a ledger around it. If you want average-cost behaviour today, keep the position in a NONE account and compute the average yourself, or track each lot and accept lot-level gains.
8. NONE
The NONE method disables lot matching entirely.
2024-01-01 open Assets:Invest:STOCK "NONE"- No Lot Matching: Beancount does not attempt to match reductions to augmentations.
- Allows Mixed Signs: This allows an account to hold both positive and negative balances of the same commodity simultaneously. This behavior is similar to how the Ledger CLI tool handles commodities.
Lot Specification
A "lot" is a specific block of a commodity acquired at a particular time and price. When you create or reduce a position, you can specify its lot attributes in detail.
Full Specification
When augmenting an inventory (buying), you can specify up to three attributes for the lot, comma-separated inside a single pair of braces:
Assets:Invest:STOCK 10 STOCK {100.00 USD, 2024-01-15, "lot-identifier"}100.00 USD— the cost basis, expressed per unit.2024-01-15— the acquisition date. Beancount fills this in from the transaction date when you omit it, which is why the error messages above show a date on every lot."lot-identifier"— an optional string label.
While all three are optional, providing at least the cost basis is standard practice. The braces must stay on one line, and comments inside a ledger start with ;, never #.
Matching Methods
When reducing an inventory (selling), you use the same syntax to specify which lot(s) to sell from.
-
Match by cost: This is the most common method.
Assets:Invest:STOCK -5 STOCK {100.00 USD} -
Match by date: If costs are identical, you can disambiguate using the acquisition date.
Assets:Invest:STOCK -5 STOCK {2024-01-15} -
Match by label: Labels provide a foolproof way to identify a lot.
Assets:Invest:STOCK -5 STOCK {"lot-identifier"} -
Leave the lot to the booking method: An empty set of braces
{}names no lot, so the account's booking method chooses. UnderFIFO,LIFOorHIFOthat is the oldest, newest or most expensive matching lot; under the defaultSTRICTit is anAmbiguousMatchErrorunless the reduction empties the matched lots exactly.Assets:Invest:STOCK -5 STOCK {}
Price Handling
It is crucial to understand the difference between cost basis ({}) and price (@). They serve different purposes and are not interchangeable.
Price vs Cost
{cost}: Defines the acquisition cost of an asset. It is part of the inventory lot itself and is used for booking reductions and calculating capital gains.@ price: An annotation that records a market price at the time of a transaction. It is used for currency conversions or to note the market value on a particular date.
Here are the three scenarios:
-
Price Annotation (Conversion): Use
@to convert from one currency to another.Assets:Forex 1000 USD @ 0.85 EUR -
Cost Basis (Acquisition): Use
{}when buying an asset to establish its cost.Assets:Invest 10 STOCK {100.00 USD} -
Both (Sale with Price Record): When selling an asset, use
{}to identify the lot being sold and@to record the sale price. This allows for automated capital gains calculation.Assets:Invest -10 STOCK {100.00 USD} @ 105.00 USDThis entry sells 10
STOCKfrom the lot that cost $100.00 each, at a sale price of $105.00 each.
Price Usage Rules
- Price annotations (
@) do not affect which lot is booked. Lot matching is handled exclusively by the cost basis ({}) and the account's booking method. - The
@symbol is used only for:
- Currency conversions.
- Recording the market value of an asset at the time of a transaction.
- Providing the sale price for capital gains calculations.
Configuration
You can configure booking methods globally or on a per-account basis.
Global Booking Method
You can set a default booking method for your entire Beancount file using the option directive.
option "booking_method" "STRICT"The accepted values are "STRICT" (the default when you set nothing), "STRICT_WITH_SIZE", "NONE", "FIFO", "LIFO", "HIFO" and "AVERAGE". Any other string is rejected at load time with Error for option 'booking_method'. "AVERAGE" is accepted here and on open, but booking a reduction under it fails, as the AVERAGE section above shows.
Per-Account Override
It's often useful to have different methods for different accounts. For example, you might want FIFO for a retirement account but STRICT for a taxable brokerage account to ensure you are selling specific tax lots. You can set the booking method when you open the account.
2024-01-01 open Assets:Retirement:401K "FIFO"
2024-01-01 open Assets:Taxable:Stock "STRICT"Best Practices
-
Inventory Organization: To keep your ledger clean and simple, it is highly recommended to use separate accounts for each unique commodity you hold, and to constrain each one to that commodity on its
opendirective.; GOOD: separate accounts by commodity, each constrained to one 2024-01-01 open Assets:Invest:VTSAX VTSAX 2024-01-01 open Assets:Invest:VFIAX VFIAXAvoid mixing different stocks or funds in the same account, as it complicates inventory management. The commodity list on
openmakes Beancount reject a stray posting instead of silently mixing two inventories. -
Lot Management:
-
Use meaningful labels for lots, especially for specific transactions like tax-loss harvesting or employee stock grants.
Assets:Invest:STOCK 10 STOCK {100.00 USD, "tax-loss-harvest-2024"} -
Document your trades with comments. This makes your ledger easier to read and understand later.
Assets:Invest:STOCK -10 STOCK {100.00 USD} @ 110.00 USD ; Gain: 10%
- Debugging: If you encounter errors or unexpected behavior, Beancount provides tools to inspect the state of your inventory.
-
Examine Inventory State: The
bean-doctortool can show you the exact state of all inventories at any point in your file.Replace
<LINENO>with the line number just after a transaction to see its effect. -
Verify Lot Matching: The
bea checktool validates your entire file. It will catch any booking errors, such as ambiguous lot matches inSTRICTmode.