Introduction

Tracking your investments can easily become a mess of PDFs, spreadsheets, and inconsistent brokerage exports. If you’re already using Beancount to manage your personal finances, why not bring that same clarity and precision to your stock portfolio?

Beancount isn’t just for daily finances—it’s a powerful tool for investment tracking. With its plain-text syntax and double-entry model, it gives you full control over cost basis, dividends, capital gains, and multi-currency holdings—without relying on a brokerage interface. It can easily become a central management tool for your investments.

In this post, we’ll walk through how to track stock transactions in Beancount:

  • Record stock purchases with accurate cost basis
  • Track dividends and reinvestments
  • Compute capital gains and losses on sales
  • Maintain a clean, auditable history of your portfolio
  • Generate reports and visualizations using tools like Fava

Whether you’re holding a few ETFs or managing a diversified portfolio, Beancount gives you transparency, consistency, and peace of mind.

The Beancount examples in this post can be found in: https://github.com/flyaway1217/beancount_example/blob/main/single_examples/stock.bean

Concepts

Before we begin writing actual transactions, it’s essential to understand several foundational concepts that underpin stock accounting in Beancount.

Cost Prices & Market Prices

When tracking investments in Beancount, we need to distinguish between two key concepts: cost price and market price. Each serves a different purpose in your accounting records and affects how you analyze your portfolio - one about the past (what you paid), and one about the present (what it’s worth now).

Cost Prices ({} notation)

Beancount uses the {} to represent the cost price.

Suppose you have a normal broker account (named Playground for example) at Fidelity and you buy 10 shares of Amazon (ticker symbol: AMZN) with the price of $200 per share:

1
2
3
2025-05-01 * "Buy 10 AMZN"
    assets:Fidelity:Cash -2,000.00 USD
    Assets:Fidelity:Playground:AMZN 10 AMZN {200.00 USD}

This record indicates:

  • You convert 2000 dollar cash into 10 shares of AMZN.
  • The cost basis per share is $200.
  • Total cash cost is $2,000.

Here I use a subaccount :AMZN under Assets:Fidelity:Playground to hold the AMZN shares, which is technically not required by Beancount. But I highly recommend that you use a separate subaccount for each stock because this makes the aggregation more clear and convenient.

After the transaction is posted, no matter how the market price changes, this cost basis remains fixed.

Market Prices

Although the cost basis is fixed when the transaction is posted, the market price of the stock keeps changing. The difference between market prices and your cost basis is your profit (or loss). For example, if next day, the price of Amazon becomes $210, Then your profit is:

10×$21010×$200=$100 10\times \$210 - 10 \times \$200 = \$100

Similarly, if the price becomes $190, you have a loss:

10×$19010×$200=$100 10\times \$190 - 10 \times \$200 = -\$100

However, as long as you do not sell your shares, these profits (or loss) are unrealized.

In Beancount, the market price is reflected by a price database, which will be discussed in Price Fluctuations.

Lots & Inventory

The above examples is the simplest one. In reality, the situation becomes more complicated. You might buy the same stock at different times with different (with high probability) prices. During which time, you might also sell some shares. How can we use Beancount to track these changes?

Beancount uses the concepts of lots and inventory to handle these situations.

Lots

A lot in Beancount is a specific acquisition of a commodity (in our case, it is 10 shares of AMZN stock), capturing details such as:

  • Quantity: Number of units acquired.
  • Cost: Price paid per unit, including the currency.
  • Acquisition Date: Date when the asset was acquired.
  • Label (Optional): A custom identifier for the lot.

Each lot is uniquely defined by its quantity, cost, acquisition date, and optional label. To merge two lots, all three attributes—cost, acquisition date, and label—must match exactly, a condition that rarely occurs naturally. As a result, lot consolidation is infrequent and requires deliberate, careful handling—a subject we’ll explore in a future post. For now, it’s sufficient to recognize that each transaction produces a distinct lot, which remains separate in your holdings unless you explicitly choose to combine them.

To be accurate, we should rewrite the above transaction using following syntax:

1
2
3
2025-05-01 * "Buy 10 AMZN"
    Assets:Fidelity:Cash -2000.00 USD
    Assets:Fidelity:Playground:AMZN 10 AMZN {200.00 USD, 2025-05-01, "Lot 1"}

Although this is also a valid syntax for Beancount, it is long and tedious. Beancount is able to automatically create the lot for us using the above simplified version. However, if you want to log the lot with some labels, you can use the full syntax.

Inventory

An inventory is the collection of all lots held within a particular account. It aggregates the various lots to represent the total holdings of a commodity. Inventories are dynamic; they increase with acquisitions (adding new lots) and decrease with disposals (reducing existing lots).

So, if you buy another 20 shares of AMZN on next day with the price of $190:

1
2
3
2025-05-02 * "Buy 20 more shares of AMZN"
    Assets:Fidelity:Cash -3600.00 USD
    Assets:Fidelity:Playground:AMZN 20 AMZN {180.00 USD}

Then, you would have an inventory with two lots for the account Assets:Fidelity:Playground:AMZN:

units currency cost cost currency lot-date label
10 AMZN 200 USD 2025-05-01 None
20 AMZN 180 USD 2025-05-02 None

These two lots cannot be merged into a single 30‑share lot because their purchase prices and acquisition dates differ. While Beancount will report your total holding as 30 shares, it actually maintains two separate lots internally—each with its own cost basis. This mirrors what you’d see in your brokerage account: a combined share total alongside detailed, individual lot information.

Beancount uses inventories to manage and report on the current state of your assets, ensuring that all transactions align with the actual holdings.

Booking Methods

Why we want to have different lots? It is mostly due to the tax-purpose.

Suppose you want to sell 5 shares of AMZN on 2025-05-03 with the price of $190. Which lot (cost) should we use to reduce the shares?

If we sell the 5 shares from the first lot, then we would have a loss of $50:

($190$200)×5=$50 (\$190 - \$200) \times 5 = -\$50

On the other hand, if we sell the 5 shares from the second lot, we would have a profit of $50.

($190$180)×5=$50 (\$190 - \$180) \times 5 = \$50

So, reducing different lots has a big impact on your taxes. How do you reduce these lots (the booking method) depends on your local tax laws, regularization and your personal financial situations. Beancount is designed to handle different situations by specifying the lots you want to reduce, which is showed in the next subsections1.

Real Transactions

Setting up Accounts

Now, after explaining the necessary concepts, let’s start writing real transactions. As usual, the first step is to set up some accounts we will use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
; Define the commodity for AMAZN
2025-01-01 commodity AMZN
; Cash account
2005-01-01 open Assets:Fidelity:Cash  USD
; The account to hold the Amazon stocks
2025-01-01 open Assets:Fidelity:Playground:AMZN  AMZN
; The account used to track the profit and loss of the investment on Amazon stock
2025-01-01 open Income:Fidelity:AMZN:PnL
; The account used to track the dividends of Amazon stock (Amazon does not distribute dividends in reality, here, we only use it for demostration)
2025-01-01 open Income:Fidelity:AMZN:Dividends
; The expenes account to track the commissions
2025-01-01 open Expenses:Financial:Commissions

Buy

Previously, we wrote some buying transaction, however, that is a simplified one. In reality, we need to consider the commission fees (as well as other possible fees). More practical buying transactions are as following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
2025-05-01 * "Buy 10 AMZN at price of 200 USD"
    Assets:Fidelity:Cash -2010.00 USD
    Assets:Fidelity:Playground:AMZN 10 AMZN {200.00 USD}
    ; I assume a fixed rate (10 dollars) for each transaction
    Expenses:Financial:Commissions 10 USD

2025-05-02 * "Buy 20 AMZN at price of 180 USD"
    Assets:Fidelity:Cash -3610.00 USD
    Assets:Fidelity:Playground:AMZN 20 AMZN {180.00 USD}
    ; I assume a fixed rate (10 dollars) for each transaction
    Expenses:Financial:Commissions 10 USD

Sell

Now, suppose we want to sell.

If you want to sell 5 shares from the first lots, you can write the transaction as:

1
2
3
4
5
2025-05-03 * "sell 5 shares from the first lot"
    Assets:Fidelity:Playground:AMZN -5 AMZN {200.00 USD} @ 190 USD
    Assets:Fidelity:Cash 950 USD
    Expenses:Financial:Commissions 10 USD
    Income:Fidelity:AMZN:PnL

This transaction says that we sell 5 shares of AMZN stock from the first lot of our inventory with per-share-price 190 dollars. As a result, we receive cash of 950 dollars and spend 10 dollars for commissions.

Beancount uses @ to represent the selling price per share and @@ to represent the total selling value of the stocks. So, the above the transaction can be equivalently written as:

1
2
3
4
5
2025-05-03 * "sell 5 shares from the first lot"
    Assets:Fidelity:Playground:AMZN -5 AMZN {200.00 USD} @@ 950 USD
    Assets:Fidelity:Cash 950 USD
    Expenses:Financial:Commissions 10 USD
    Income:Fidelity:AMZN:PnL

You might note that we include the account Income:Fidelity:AMZN:PnL into the transaction. The transaction is not balanced without it. As the name suggested (PnL: Profit and Loss), this account is used track your actual profit or loss. This is also the number that will be taxed on. We do not put the actual number because we let the Beancount to fill in it for us. We can also compute it by manually computation:

5×$(190200)$10=$50 -5 \times \$(190-200) - \$10 = -\$50

We lose $50 if we sell the 5 shares from the first lot.

However, when we reduce the stocks from second lot, we would end up a profit:

1
2
3
4
5
2025-05-03 * "sell 5 shares from the second lot"
    Assets:Fidelity:Playground:AMZN -5 AMZN {180.00 USD} @ 190 USD
    Assets:Fidelity:Cash 950 USD
    Expenses:Financial:Commissions 10 USD
    Income:Fidelity:AMZN:PnL

Beancount automatically searches the matched lot from our inventory to reduce. In our case, it searches for the lot with cost price of 180 dollars. (It searches for the cost price of 200 dollars if we reduce from the first lot.) In most cases, this is enough. However, if you want, you can also put the date in the transaction to further specify the lot you want to reduce. For example, the above transaction can also be written as:

1
2
3
4
5
2025-05-03 * "sell 5 shares from the second lot"
    Assets:Fidelity:Playground:AMZN -5 AMZN {180.00 USD, 2025-05-02} @ 190 USD
    Assets:Fidelity:Cash 950 USD
    Expenses:Financial:Commissions 10 USD
    Income:Fidelity:AMZN:PnL

We will talk more about lot searching and matching in a separate blog post.

A Note on @ and @@

So far, we almost complete the stock transactions in Beancount. One last thing I want to mention is the symbols of @ and @@. These two symbols actually is not necessary in the syntax. In other words, we can simply remove them and the transaction is still valid. They are there mostly for reading convenient. Beancount does not use them because Beancount does not record the selling price. All Beancount does is to compare the total cost of the stocks and the total cash we received in this transaction, and based on these two numbers, Beancount knows if we have a profit or a loss.

Dividends

Sometimes, the stock (or ETF) distributed dividends. This is easily can be recorded as:

1
2
3
2025-06-01 * "Amazon Dividends"
    Assets:Fidelity:Cash 10 USD
    Income:Fidelity:AMZN:Dividends -10 USD

While Amazon does not pay dividends at the time of writing, this example demonstrates the general structure.

Price Fluctuations

The last piece of stock transactions in Beancount is to track the price of stocks. Beancount supports and maintains an internal price database to reflect the prices changes. The syntax is as following:

1
2
2025-06-03 price AMZN                   205.73 USD
2025-06-07 price AMZN                   213.75 USD

It records the closing prices for AMAZON on 2025-06-03 and 2025-06-07. We can keep adding the prices logs. Beancount provides a simple plugin to help us fetch the prices from various sources.

Beancount itself does not really use this price database. Its core functionality does not rely on these prices logs. But some plugins use this information to enhance the analysis and visualizations, which will be covered in other posts in the future.

Final Words

Beancount offers a powerful, transparent, and extensible approach to investment tracking. By modeling cost basis, lots, and realized gains explicitly, it brings accounting-level precision to personal portfolio management.

Unlike spreadsheets or proprietary brokerage platforms, Beancount gives you full control over your financial records. Every transaction is traceable, auditable, and version-controlled—ideal for long-term investors who value clarity and autonomy.

If you already use Beancount for budgeting or expense tracking, extending it to your investments is a natural next step—bringing everything under a unified, consistent system.

Appendix

More Selling Examples: Reduce a Mix of Lots

In the main sell section, we demonstrated that we can choose to reduce 5 shares of AMZN from either the first lot or the second lot. Actually, Beancount is more flexible than that: we can even reduce a mix of these two lots, e.g., 2 shares from the first lot and 3 shares from the second lot:

1
2
3
4
5
6
2025-05-03 * "sell 2 shares from the first lot and shares from the second lot"
    Assets:Fidelity:Playground:AMZN -2 AMZN {200.00 USD} @ 190 USD
    Assets:Fidelity:Playground:AMZN -3 AMZN {180.00 USD} @ 190 USD
    Assets:Fidelity:Cash 950 USD
    Expenses:Financial:Commissions 10 USD
    Income:Fidelity:AMZN:PnL

This granularity allows you to optimize your tax position or match specific bookkeeping requirements.

References


  1. Beancount supports a few common booking methods, such as FIFO or LIFO, but this is beyond the scope of this post. We will cover these topics in future posts. ↩︎