Why I Finally Migrated from hledger to Beancount (And Why Lot Tracking Was the Clincher)

After four years with Beancount, I can confidently say it was the right choice—but I didn’t start here. Like many in the plain text accounting world, my journey began with hledger. And honestly? hledger served me well for a solid 18 months before I felt the pull to migrate.

Let me share why I made the switch, and why investment lot tracking ended up being the deciding factor.

The hledger Years: What Worked

When I first discovered plain text accounting in 2022, I was tracking basic expenses and income across a few checking accounts and credit cards. hledger’s flexible, Ledger-compatible syntax felt natural. The command-line reports were fast, the ecosystem was welcoming, and I appreciated the “forgiving friend” philosophy—I could get started without worrying about declaring every account upfront.

For simple personal finance tracking, hledger absolutely delivered. I loved hledger bal, hledger reg, and the straightforward CSV import process. Everything just worked.

The Turning Point: Investments Enter the Picture

Then my financial life got more complex. I started seriously investing for financial independence, added two rental properties, and suddenly needed to track:

  • Cost basis for individual stock and ETF lots (for capital gains reporting)
  • Tax-loss harvesting opportunities across taxable brokerage accounts
  • Wash sale tracking to avoid IRS violations
  • Rental property expenses allocated correctly for depreciation

This is where I hit hledger’s limits. While hledger technically supports some lot syntax, it doesn’t have the robust infrastructure Beancount offers. Specifically:

Technical Comparison: Lot Syntax

hledger approach:

  • Limited lot syntax support (can parse {...} but doesn’t do much with it)
  • Manual workarounds using separate accounts for each lot
  • No built-in capital gains calculation
  • Cost basis tracking requires custom scripting

Beancount approach:

  • Native {} syntax for lot identification: 100 AAPL {150.00 USD}
  • Built-in support for FIFO, LIFO, and specific lot identification
  • Automatic unrealized gains calculation
  • The fava_investor plugin for tax-loss harvesting identification
  • Balance assertions work seamlessly with lots

Here’s a real example. In hledger, tracking a stock purchase looked like this:

2024-01-15 Buy AAPL
    Assets:Brokerage:AAPL-Lot1    100 AAPL @ 150.00 USD
    Assets:Brokerage:Cash        -15000 USD

But then I’d have to manually create a new account for every lot and track cost basis in spreadsheets. Not sustainable.

In Beancount, it’s elegant:

2024-01-15 * "Buy AAPL"
  Assets:Brokerage:Cash         -15000.00 USD
  Assets:Brokerage:AAPL          100 AAPL {150.00 USD}

When I later sell specific lots, Beancount automatically calculates my gain/loss:

2024-06-15 * "Sell AAPL - Tax Loss Harvest"
  Assets:Brokerage:AAPL          -50 AAPL {150.00 USD} @ 140.00 USD
  Assets:Brokerage:Cash          7000.00 USD
  Income:CapitalGains:LongTerm   -500.00 USD

The system knows which lot I’m selling and tracks the $500 loss automatically.

The Migration Process

Once I decided to migrate, I used the hledger2beancount tool, which worked surprisingly well:

hledger print -f main.journal -O beancount > beancount.bean

The conversion handled about 85% of my transactions perfectly. I spent a weekend cleaning up:

  • Adding explicit account opening declarations
  • Fixing balance assertions (Beancount is stricter)
  • Converting some hledger-specific tags to Beancount metadata
  • Restructuring my lot tracking to use proper syntax

Time investment: ~10 hours total, spread over a week.

Biggest surprise: How much cleaner my ledger became. Beancount’s “strict teacher” approach forced me to fix inconsistencies I didn’t realize existed.

What Beancount Enables Now

Fast forward to today. Here’s what I can do now that would’ve been painful (or impossible) in hledger:

  1. Tax-loss harvesting dashboard: The fava_investor plugin highlights loss opportunities in real-time
  2. Accurate Form 8949 reporting: Cost basis for every sale, automatically calculated
  3. Wash sale tracking: Custom queries flag potential violations across accounts
  4. Portfolio rebalancing: Asset allocation reports that factor in unrealized gains
  5. Rental property P&L: Property-level income statements with proper expense categorization

The Fava web interface is the cherry on top. My spouse can now view our financial dashboard without touching the command line, which has been a relationship win.

Advice for Others Considering the Switch

You should stick with hledger if:

  • You’re primarily tracking basic income/expenses without complex investments
  • You love the command-line workflow and minimal setup
  • You prefer hledger’s flexibility over Beancount’s strictness
  • You’re happy with your current workflow

Consider Beancount if:

  • You’re actively investing and need precise cost basis tracking
  • Tax-loss harvesting or capital gains reporting matters to you
  • You want a polished web UI (Fava) for visualizations
  • You appreciate catching data errors early through strict validation
  • You plan to automate imports and need Python ecosystem integration

No Regrets

I have nothing but respect for the hledger community and the tool itself. For the right use case, hledger absolutely shines. But for my needs—serious investment tracking, tax optimization, and audit-ready records—Beancount’s lot tracking capabilities were the clincher.

The migration wasn’t trivial, but it wasn’t scary either. And the payoff has been worth every hour invested.

If you’re on the fence, my advice: start simple with whichever tool resonates with you. But when your financial life gets complex—especially if investments are involved—take a serious look at Beancount’s lot tracking. It might just be the feature that tips the scale.


Have you migrated between plain text accounting systems? What was your deciding factor? Let’s hear your stories below.

This resonates so much with my FIRE journey! I’ve been using Beancount for about 2 years now, and lot tracking was similarly critical for me—though I came from Mint/spreadsheets rather than hledger.

Your example with the AAPL lot syntax is chef’s kiss. That automatic capital gains calculation on the sale transaction is exactly what makes Beancount irreplaceable for tax-optimized investing.

My Tax-Loss Harvesting Workflow

Mike, you mentioned the fava_investor plugin for TLH—I’d love to hear more about your specific workflow. Here’s what I do:

  1. Monthly TLH reviews: I run fava_investor reports in Fava to identify lots trading below cost basis
  2. Wash sale vigilance: Before selling, I check if I’ve bought the same security (or “substantially identical”) within 30 days in ANY account (taxable, IRA, HSA)
  3. Replacement purchases: For harvested losses, I immediately buy a similar (but not identical) ETF to maintain market exposure
  4. Tracking in Beancount: I tag TLH transactions with #tlh and include the wash sale period in comments

What I’m still figuring out:

Question 1: How do you handle wash sale tracking across multiple brokerage accounts? I have Vanguard, Fidelity, and Robinhood all in Beancount, and manually checking 30-day purchase windows across all three is tedious. Have you written a custom query or plugin to flag potential wash sales automatically?

Question 2: For Form 8949 reporting, do you export directly from Beancount or use an intermediate tool? I’ve been manually copying cost basis data into TurboTax, but there has to be a better way.

Cost Basis Reporting Is Everything

As someone tracking toward CoastFIRE (hit my number last year!), accurate cost basis isn’t just about taxes—it’s about confidence in my withdrawal planning. When I model various retirement scenarios, I need to know:

  • Which lots I can sell tax-efficiently (long-term vs short-term)
  • How much unrealized gain I’m sitting on (affects safe withdrawal rate assumptions)
  • What my tax liability will be when I start drawing down

hledger couldn’t give me that. Spreadsheets were error-prone and became unmanageable past ~50 lots. Beancount + Fava makes this visible and trustworthy.

Your point about Fava being a “relationship win” is spot-on. My partner was skeptical of my obsessive tracking until I showed them the Fava dashboard. Now they check our net worth progress weekly!

Thanks for sharing your migration story—it’s a great resource for folks still on the fence. The “start simple” advice is gold. I see too many newcomers trying to model complex investment structures on day one and getting overwhelmed.

For those reading: If you’re tracking even a modest investment portfolio (1-2 brokerage accounts, 5+ holdings), Beancount’s lot tracking will save you hours every tax season and give you peace of mind that your cost basis is accurate.

Mike, this is an excellent breakdown of the technical differences. As a CPA who’s helped several clients migrate from various systems to Beancount, I want to emphasize just how critical accurate lot tracking is—not just for tax optimization, but for audit preparedness.

The IRS Cares About Cost Basis Documentation

When the IRS audits investment income (and they do, especially for taxpayers with significant capital gains), they want to see:

  1. Clear acquisition dates and costs for every security sold
  2. Consistent lot identification methods (FIFO, LIFO, or specific identification)
  3. Supporting documentation (brokerage statements, trade confirmations)

What I love about Beancount’s lot tracking is that it creates an audit trail by design. Every transaction includes the lot cost, and with proper comments/metadata, you can link back to source documents. When a client gets that IRS notice, we can generate a complete history of any position in minutes.

With hledger’s manual lot tracking (or worse, spreadsheets), I’ve seen clients scramble to reconstruct cost basis years after the fact. Not fun.

Real Client Migration Story

Last year, I had a client who was managing a $400K investment portfolio in hledger using the “separate account per lot” workaround Mike described. They had over 200 accounts just for tracking individual stock and ETF purchases. The ledger was a mess:

  • Assets:Brokerage:VTSAX:Lot-2022-03-15
  • Assets:Brokerage:VTSAX:Lot-2022-04-12
  • Assets:Brokerage:VTSAX:Lot-2022-05-08
  • (… 197 more accounts)

We migrated to Beancount over a weekend. Now they have:

  • One Assets:Brokerage:VTSAX account
  • Proper lot syntax on every transaction
  • Automatic unrealized gains calculation
  • Clean, maintainable records

The difference is night and day. And when tax season arrived, generating Form 8949 data was trivial instead of a multi-day reconstruction project.

Balance Assertions Are Your Friend

One underrated benefit of Beancount’s lot tracking: balance assertions work seamlessly with commodity positions. You can assert not just the quantity of shares, but verify your cost basis matches brokerage statements:

2024-12-31 balance Assets:Brokerage:VTSAX  125.5 VTSAX

If your lot tracking is off (e.g., you fat-fingered a cost), Beancount will catch it immediately. This gives me confidence as a preparer that the data I’m relying on for tax returns is reconciled and accurate.

Professional Advice for Investors

If you’re managing investments in plain text accounting:

  • Start with lot tracking from day one, even if you think your portfolio is “too simple” to need it. It’s much harder to retrofit later.
  • Reconcile against brokerage statements quarterly—don’t wait until tax season to discover discrepancies.
  • Use specific identification for tax-loss harvesting. FIFO is simpler, but specific ID gives you control over which lots to sell for tax optimization.
  • Keep source documents—Beancount’s document directive is perfect for linking trade confirmations and statements.

Mike, thank you for sharing this. I’m bookmarking your post to share with clients who ask about plain text accounting for investments. The technical comparison between hledger and Beancount lot syntax is spot-on and will help folks make informed decisions.

Alice absolutely nailed the audit perspective. As a former IRS auditor turned tax preparer, I can tell you that cost basis documentation is one of the most scrutinized areas during investment audits. Mike’s migration story hits on something critical that many investors don’t realize until it’s too late.

Form 8949: The Capital Gains Reporting Reality

When you sell investments, you’re required to report every transaction on Form 8949 (Sales and Other Dispositions of Capital Assets), which then rolls up to Schedule D. The IRS receives cost basis information directly from brokerages via Form 1099-B, and they will notice if your reported numbers don’t match.

What makes Beancount invaluable here:

  • Automatic gain/loss calculation per transaction (as Mike showed with the AAPL example)
  • Clear lot identification that distinguishes short-term vs long-term gains
  • Audit trail showing acquisition date, cost, sale date, and proceeds
  • Consistent methodology across all transactions (no switching between FIFO and specific ID mid-year)

With hledger’s manual approach, I’ve seen clients accidentally report:

  • Wrong acquisition dates (affecting short vs long-term classification)
  • Incorrect cost basis (miscalculated or misremembered)
  • Inconsistent lot identification methods (IRS red flag)

These aren’t just paperwork mistakes—they’re potential penalties and interest if the IRS determines you underreported gains.

The Wash Sale Rule Minefield

Fred asked about wash sale tracking—this is where things get really tricky, and it’s an area where Beancount’s structure can help (but requires vigilance).

Quick primer: The wash sale rule disallows deducting a loss if you buy the same or “substantially identical” security within 30 days before OR after the sale. The loss isn’t gone—it gets added to the cost basis of the replacement shares—but it can’t be deducted in the year of sale.

The challenge: Wash sales apply across ALL accounts you control:

  • Taxable brokerage accounts
  • IRAs (traditional and Roth)
  • 401(k)s
  • Spouse’s accounts (if filing jointly)
  • Even HSAs if you’re investing in them

Fred, for your multi-account scenario (Vanguard, Fidelity, Robinhood), here’s what I recommend:

  1. Tag all purchases and sales with security symbols in Beancount metadata
  2. Write a custom BQL query that searches for purchases of the same security within 30 days of any sale
  3. Flag potential wash sales for manual review (“substantially identical” is a judgment call)
  4. Adjust cost basis in Beancount when wash sales occur

I don’t have a turnkey solution, but Beancount’s query language makes this possible in a way that spreadsheets don’t.

Cryptocurrency: The Wild West of Cost Basis

One area where Beancount’s lot tracking is essential: cryptocurrency. The IRS treats crypto as property, meaning every trade (even crypto-to-crypto) is a taxable event requiring cost basis tracking.

I’ve had clients with hundreds of crypto trades across multiple exchanges. Without lot-level tracking:

  • They had no idea which tax lots they were selling
  • Cost basis reconstruction was impossible after exchanges deleted old data
  • Tax bills were shocking because they couldn’t prove their actual basis

Beancount’s lot syntax works perfectly for crypto:

2024-03-15 * "Buy Bitcoin"
  Assets:Coinbase:BTC           0.5 BTC {50000.00 USD}
  Assets:Coinbase:Cash      -25000.00 USD

2024-09-20 * "Sell Bitcoin - Specific Lot"
  Assets:Coinbase:BTC          -0.5 BTC {50000.00 USD} @ 48000.00 USD
  Assets:Coinbase:Cash       24000.00 USD
  Income:CapitalGains:ShortTerm -1000.00 USD

The loss is documented, the holding period is clear, and the audit trail is complete.

Quarterly Estimated Taxes for Active Traders

If you’re actively trading (not just buy-and-hold), you need to make quarterly estimated tax payments on realized gains. Beancount’s real-time tracking lets you:

  • Calculate year-to-date realized gains/losses at any time
  • Project tax liability based on current activity
  • Make informed quarterly payment decisions
  • Avoid underpayment penalties

This is light-years ahead of waiting until January to discover you owe five figures and missed quarterly deadlines.

Final Tax Professional Recommendation

If you’re investing with Beancount:

  • Use specific lot identification from day one (it’s the most flexible)
  • Document everything—comments in transactions, linked brokerage statements
  • Reconcile monthly—don’t let discrepancies compound
  • Consult a tax pro before making large sales or harvesting losses

Mike, your migration story is a fantastic resource. The hledger vs Beancount comparison is exactly what investors need to see to understand why proper lot tracking matters. Thanks for taking the time to write this up!

Alice and Tina, thank you both for the professional insights! This is exactly the kind of expertise I was hoping to tap into.

My fava_investor Setup

Since you asked, Mike, here’s my current fava_investor configuration. I have it enabled in my main.bean file:

plugin "fava_investor.modules.tlh" "{
  'account_field': 'account_type',
  'loss_threshold': 500
}"

This gives me a dedicated “Tax Loss Harvesting” tab in Fava that shows:

  • All lots currently trading below their cost basis
  • Potential loss amount for each lot
  • Whether the position is short-term or long-term
  • Easy filtering by account

The loss_threshold: 500 setting means I only see opportunities worth at least $500—keeps the noise down.

Responding to Tina’s Wash Sale Guidance

Your recommendation to use BQL queries for wash sale detection is brilliant. I haven’t built a comprehensive solution yet, but here’s a starting point I use to flag potential issues:

SELECT date, account, position, cost(position) AS cost_basis
WHERE account ~ 'Assets:Brokerage'
  AND year = 2026
ORDER BY date

This gives me a chronological view of all purchases/sales. I then manually review for securities purchased within 30 days of a TLH sale. It’s not automated, but it’s faster than scanning through my entire ledger.

Question for the group: Has anyone built a more sophisticated wash sale detection plugin or query? I’d be happy to contribute to or test something like this if there’s community interest.

My Example Account Structure

For anyone curious, here’s how I structure investment accounts in Beancount:

Assets:Brokerage:Vanguard:Cash
Assets:Brokerage:Vanguard:VTSAX
Assets:Brokerage:Vanguard:VTIAX
Assets:Brokerage:Fidelity:Cash
Assets:Brokerage:Fidelity:FXAIX
Assets:Retirement:IRA:Traditional:Cash
Assets:Retirement:IRA:Traditional:VTSAX
Assets:Retirement:IRA:Roth:Cash
Assets:Retirement:IRA:Roth:VTI

I keep taxable and retirement accounts strictly separated because the tax treatment is so different. Each holding gets its own account (e.g., VTSAX, VTIAX) rather than a generic “Stocks” bucket—this makes lot tracking cleaner and Fava reports more granular.

Migration Resources

For folks considering an hledger → Beancount migration, I wrote up my experience (including conversion scripts and gotchas) on my FIRE blog: Fred’s FIRE Journey: Plain Text Accounting Migration (not linking directly to avoid spam filters, but Google “Fred Chen FIRE blog” if interested).

The TL;DR:

  • Use hledger print -O beancount as starting point
  • Expect to spend 5-15 hours on cleanup depending on ledger complexity
  • Start with a subset of data (e.g., last year only) to learn Beancount’s strictness
  • Balance assertions will catch most conversion errors early

Gratitude to This Community

This thread is a perfect example of why I love the Beancount community. We’ve got:

  • Mike’s honest, detailed migration story with code examples
  • Alice’s professional CPA perspective on audit preparedness
  • Tina’s former-IRS-auditor insights on what actually matters for taxes
  • Practical, no-BS advice that helps real people make better financial decisions

I’m saving this thread as a reference for when friends ask me why I obsess over plain text accounting. Thank you all!