Tax-Loss Harvesting Automation: Using Beancount to Track Wash Sale Rules

Tax season is around the corner, and I’ve been diving deep into tax-loss harvesting strategies. But here’s the thing that keeps me up at night: wash sale violations. They’re like hidden landmines in your investment portfolio, and if you’re not careful, they can blow up your tax-saving strategies.

The 61-Day Window of Doom

Most people think it’s just a “30-day rule,” but the reality is more complex. The IRS wash sale rule creates a 61-day window around every sale:

  • 30 days BEFORE the sale
  • The day of the sale itself
  • 30 days AFTER the sale

If you sell a security at a loss and purchase a “substantially identical” security anywhere within that 61-day window, the IRS disallows your loss deduction. And here’s the kicker: it applies across ALL your accounts — your Fidelity brokerage, your Vanguard IRA, your spouse’s 401(k), everything.

The Brokerage Reporting Gap

Here’s what I learned the hard way: brokers only track wash sales within their own accounts.

Last year, I sold VTI (Vanguard Total Stock Market ETF) at a $3,200 loss in my Fidelity taxable account. Smart tax move, right? Wrong. Two weeks later, my automated investment plan at Vanguard bought VTSAX (Vanguard Total Stock Market Index Fund) — which the IRS considers “substantially identical” to VTI.

My Fidelity 1099-B looked clean. My Vanguard 1099-B looked clean. But when I prepared my taxes, I realized I had triggered a wash sale that neither broker flagged. I had to manually disallow the loss on Form 8949 and adjust my cost basis. Painful lesson.

Why This Matters for Beancount Users

This is where Beancount can be a game-changer — IF we use it correctly. We have the power to track every transaction across every account with precise metadata. We can run queries that span the entire 61-day window. We can catch wash sales BEFORE they happen, not after.

But I’m still figuring out the best approach. Here’s what I’m struggling with:

My Current Challenge

I have:

  • 3 taxable brokerage accounts (Fidelity, Vanguard, Schwab)
  • 2 IRAs (Fidelity Roth, Vanguard Traditional)
  • My spouse has 2 accounts (401k and IRA)

When I want to harvest a loss, I need to check:

  1. Did I buy this security (or something “substantially identical”) in the past 30 days in ANY account?
  2. Do I have any scheduled purchases (auto-invest, DRIPs, 401k contributions) in the next 30 days?
  3. Did my spouse buy anything similar?

Right now, I’m doing this manually with spreadsheets and it’s error-prone.

What I Want to Build

I’m envisioning a Beancount-based system where I can:

  1. Run a pre-trade query that answers: “Is it safe to sell security X at a loss today?”
  2. Get warnings about potential wash sales before executing the trade
  3. Track cost basis by lot for every position across all accounts
  4. Maintain a mapping of “substantially identical” securities (VTI ↔ VTSAX, SPY ↔ VOO, etc.)

My Questions for the Community

I need your collective wisdom:

Account Structure: How should I structure my Beancount accounts to track multiple brokers and account types (taxable vs. IRA)?

Assets:Investments:Fidelity:Taxable:VTI
Assets:Investments:Fidelity:Roth:VTI

Is this granular enough?

Metadata Strategy: What metadata should I attach to each stock transaction?

ticker: "VTI"
cusip: "922908769"
lot_date: 2024-03-15
cost_basis_per_share: 185.50
account_type: "taxable"

What am I missing?

Query Approach: Has anyone written a BQL query to detect potential wash sales across a 61-day window? How do you handle the lookback AND look-forward?

Substantially Identical Definition: The IRS is vague about what counts as “substantially identical.” Obviously, same ticker = identical. But what about:

  • VTI (ETF) vs VTSAX (mutual fund) — both Vanguard Total Market
  • SPY vs VOO vs IVV — all S&P 500 index funds
  • Different share classes of the same fund

How are you tracking these relationships in Beancount?

IRA Coordination: How do you make sure you don’t accidentally trigger a wash sale between taxable and IRA accounts? (Especially important since IRA wash sales are PERMANENTLY disallowed, not just deferred!)

Why I’m Obsessed With This

I’m on a FIRE (Financial Independence, Retire Early) path, and tax optimization is a significant lever. Tax-loss harvesting can save thousands per year, but only if done correctly. One wash sale violation can wipe out months of careful planning.

I believe Beancount’s plain-text, query-able, version-controlled approach is perfect for this problem. We just need to develop the right conventions and tooling.

Has anyone built something like this? What’s working for you? What pitfalls have you discovered?

Let’s build a community knowledge base around tax-loss harvesting best practices with Beancount. Share your experiences, your code, your mistakes — let’s learn together.


Disclaimer: I’m not a tax professional. This is my personal experience and learning journey. Consult a CPA or tax advisor for your specific situation.

Fred, this is EXACTLY the kind of proactive tax planning I wish more of my clients did. You’re absolutely right to be concerned about wash sales — they’re a compliance nightmare, and the consequences are more severe than most people realize.

The IRA Wash Sale Trap

Let me start with the scariest part: wash sales involving IRAs are PERMANENTLY disallowed, not just deferred.

If you sell a stock for a loss in your taxable account and then buy it (or something substantially identical) in your IRA or 401(k) within the 30-day window, you lose that tax loss forever. It doesn’t get added to your cost basis. It just vanishes. I’ve seen clients lose five-figure deductions this way.

And yes, this applies to:

  • Your spouse’s IRA/401(k)
  • Your own IRA (Roth or Traditional)
  • Automated 401(k) contributions that buy the same fund
  • Dividend reinvestment plans (DRIPs) in retirement accounts

IRS Enforcement Is Getting Stricter

The IRS strengthened wash sale enforcement starting in 2025. They’re now more rigorously tracking violations across multiple accounts, including spouse accounts and tax-deferred plans. The cross-account gap you identified — where brokers only track same-account wash sales — is increasingly on their radar.

What this means practically:

  • Brokers report same-account wash sales on Form 1099-B (Box 1g with code “W”)
  • YOU are responsible for identifying and reporting cross-account wash sales
  • Form 8949 and Schedule D are where you self-report these adjustments
  • In an audit, the burden of proof is on YOU to demonstrate compliance

Why Beancount Is Perfect for This

This is honestly one of the best use cases for Beancount I’ve seen. Here’s why:

1. Transaction-level documentation with precise dates
Every buy and sell has an exact timestamp, which is critical for the 61-day window calculation.

2. Metadata for tracking securities across accounts
You can tag every transaction with the information needed for wash sale detection.

3. Query capability to identify potential violations
BQL (Beancount Query Language) can span your entire financial life, across all accounts and all time periods.

4. Audit trail that satisfies IRS requirements
Plain text, version-controlled, timestamped changes = perfect documentation for disputes.

My Recommended Metadata Strategy

Based on what I implement for clients who track with Beancount, here’s what I recommend:

2026-01-15 * "Sold VTI at loss" ^tax-loss-harvesting
  Assets:Investments:Fidelity:Taxable:VTI  -100 VTI @ 195.50 USD
    ticker: "VTI"
    cusip: "922908769"
    acquisition_date: 2024-03-15
    cost_basis_per_share: 210.25 USD
    account_classification: "taxable"
    sale_type: "loss"
    realized_loss: 1475.00 USD
  Assets:Investments:Fidelity:Cash  19550.00 USD
  Income:Investments:CapitalLoss  -1475.00 USD

Key metadata tags:

  • ticker: The trading symbol
  • cusip: The 9-character security ID (more precise than ticker for funds with multiple share classes)
  • acquisition_date: When you originally bought this lot (critical for wash sale window)
  • account_classification: “taxable”, “ira”, “401k”, “roth”, “spousal_ira”, etc.
  • sale_type: “gain” or “loss” (helps filter candidates for wash sale checking)

For purchases, add:

  • substantially_identical_to: List related securities, e.g., "VTI,VTSAX,VITSX"

The “Substantially Identical” Problem

The IRS intentionally keeps this vague, and there’s no bright-line rule. My conservative guidance:

Definitely substantially identical:

  • Same ticker = identical
  • VTI (ETF) ↔ VTSAX (mutual fund) tracking the same index = identical
  • SPY ↔ VOO ↔ IVV (all S&P 500) = identical
  • Different share classes of the same fund = identical

Gray areas (I advise treating as identical):

  • Total market index from different providers (VTI vs. ITOT)
  • S&P 500 index from different providers
  • Sector ETFs tracking the same index

Safe replacements (NOT identical):

  • S&P 500 → Russell 1000 (similar but different index)
  • Total market → S&P 500 (overlapping but different composition)
  • Individual stock → sector ETF containing that stock

When in doubt, be conservative. The penalty for guessing wrong is losing your tax deduction.

Calendar Days, Not Trading Days

One critical detail: the 30-day window is calendar days, not trading days.

So if you sell on a Friday, 30 days later includes weekends and holidays. Don’t make the mistake of counting trading days — that’s a common error I see in DIY tax prep.

My Offer

I’ve built tax report templates for Beancount that handle wash sale identification and Form 8949 preparation. I’m happy to share the template structure and BQL queries if that would help the community.

The year-end scramble is real, but quarterly wash sale audits are way less stressful than discovering violations in March when you’re trying to file.

Keep up the excellent proactive work, Fred. You’re on the right track.

Oh man, Fred, I feel your pain. I learned about the cross-account wash sale problem the hard way about two years ago. Cost me a $2,800 deduction I thought I had locked in. Never again.

The good news? I’ve built a system that actually works. Been using it successfully since 2024, and I haven’t had a single wash sale violation since. Let me share what I do.

My Current Setup

Here’s the account structure I settled on after some trial and error:

Assets:Investments:Fidelity:Taxable:VTI
Assets:Investments:Fidelity:Taxable:Cash
Assets:Investments:Fidelity:RothIRA:VTI
Assets:Investments:Fidelity:RothIRA:Cash
Assets:Investments:Vanguard:Taxable:VTSAX
Assets:Investments:Vanguard:Taxable:Cash
Assets:Investments:Vanguard:TraditionalIRA:VTSAX
Assets:Investments:Spouse:Company401k:VanguardTotalMarket

The key insight: separate taxable from tax-deferred at the account level, not just in metadata. This makes queries much simpler because I can filter by account pattern.

My Metadata Strategy

For every single stock buy or sell, I tag it with:

2026-01-10 * "Bought VTI"
  Assets:Investments:Fidelity:Taxable:VTI  50 VTI @ 215.30 USD
    ticker: "VTI"
    cusip: "922908769"
    lot_date: 2026-01-10
    cost_basis: 215.30
    substantially_identical: "VTI,VTSAX,VITSX"
  Assets:Investments:Fidelity:Taxable:Cash  -10765.00 USD

The substantially_identical tag is crucial. I maintain a reference list of equivalencies:

  • Total market: VTI, VTSAX, VITSX, ITOT, FSKAX
  • S&P 500: SPY, VOO, IVV, SPLG, VFIAX
  • Developed international: VEA, VTMGX, IEFA

Whenever I add a new holding, I research what’s “substantially identical” and update my reference.

The Pre-Trade Check Query

Before I sell anything at a loss, I run this BQL query (simplified version):

SELECT 
  date, 
  account, 
  position, 
  META('ticker') as ticker,
  META('substantially_identical') as related
WHERE 
  account ~ 'Assets:Investments'
  AND META('ticker') = 'VTI' 
  AND date >= 2026-02-10  -- 30 days before today
  AND date <= 2026-04-11  -- 30 days after today

This shows me ALL transactions involving VTI (or anything I’ve tagged as substantially identical to it) across the 61-day window.

Important: For the “look-forward” part, I also check:

  • My automated investment schedules (I track these in a separate text file)
  • My spouse’s 401(k) contribution schedule
  • Any dividend reinvestment settings

If I see ANY purchase within that window, I either:

  1. Wait until the window clears, OR
  2. Temporarily pause the automated investment, OR
  3. Choose a different security to harvest (maybe harvest a loss on international instead of domestic)

The “Substantially Identical” Mapping File

I created a separate reference file (substantially-identical-securities.md) that I keep updated:

# Substantially Identical Securities for Wash Sale Tracking

## U.S. Total Stock Market
- VTI (Vanguard Total Stock Market ETF)
- VTSAX (Vanguard Total Stock Market Index Admiral)
- VITSX (Vanguard Total Stock Market Index Institutional)
- ITOT (iShares Core S&P Total U.S. Stock Market ETF)
- FSKAX (Fidelity Total Market Index Fund)

Safe replacement: Russell 3000 (different index composition)

## U.S. S&P 500
- SPY (SPDR S&P 500 ETF)
- VOO (Vanguard S&P 500 ETF)
- IVV (iShares Core S&P 500 ETF)
- VFIAX (Vanguard 500 Index Admiral)
- FXAIX (Fidelity 500 Index)

Safe replacement: Russell 1000 (similar but different index)

This is version-controlled alongside my Beancount ledger. When I’m about to harvest a loss, I consult this file to know what I need to check.

What I Check Before Every Loss Sale

My ritual (literally have a checklist):

  1. :white_check_mark: Run the BQL query for the specific ticker across 61-day window
  2. :white_check_mark: Check automated investments — do I have any scheduled purchases of this (or equivalent) in next 30 days?
  3. :white_check_mark: Check spouse’s accounts — does she have auto-investing in 401(k) that might buy this?
  4. :white_check_mark: Check DRIPs — are dividends set to auto-reinvest in this position?
  5. :white_check_mark: Document the check — I add a note to the ledger transaction with the check date

If everything’s clear, I execute the trade and immediately add it to Beancount with full metadata.

The One Thing I Wish I’d Known Earlier

Start simple. Don’t try to automate everything on day one.

My first attempt was building a complex Python script that would auto-detect wash sales. Spent 20 hours on it, got frustrated, gave up.

Then I realized: I only do tax-loss harvesting 5-10 times per year. Running a manual BQL query and checking a reference file takes 5 minutes. That’s totally sustainable.

Once you have the habit and the structure, THEN you can build automation if you want. But the manual process with good metadata is 95% of the value.

Quarterly Reconciliation

Every quarter, I run a comprehensive wash sale audit:

SELECT 
  date,
  account,
  META('ticker') as ticker,
  META('sale_type') as sale_type,
  META('realized_loss') as loss
WHERE 
  account ~ 'Assets:Investments'
  AND META('sale_type') = 'loss'
  AND date >= 2026-01-01

This gives me every loss sale in the year. Then I manually check each one against the 61-day window to verify I didn’t accidentally trigger a wash sale.

Quarterly reviews prevent year-end panic. Trust me on this.

My Offer

If folks are interested, I can share my full BQL query template and the substantially-identical reference file. Happy to help others avoid the mistakes I made.

Tina’s tax report templates sound amazing too — maybe we could combine forces and build a comprehensive tax-loss harvesting toolkit for the community?

The bottom line: It’s totally doable with Beancount. You don’t need fancy robo-advisor software. You just need discipline, good metadata, and quarterly check-ins.

You’re on the right path, Fred. Keep asking good questions!

This thread is gold. Fred, you’re asking exactly the right questions, and Tina and Mike have shared excellent practical advice. Let me add the CPA perspective on how to make this audit-ready.

Why I Recommend Beancount for Tax Clients

As someone who works with small business owners and individuals doing tax-loss harvesting, I can’t overstate how valuable proper documentation is. When the IRS comes calling, you need to prove:

  1. The date and amount of each sale
  2. The date and amount of each purchase within the 61-day window
  3. Your cost basis calculations
  4. Why you believe securities are (or aren’t) substantially identical
  5. Your wash sale adjustments

Beancount’s plain-text, timestamped, version-controlled approach gives you all of this automatically. That’s powerful.

My Client Implementation Framework

For clients who do tax-loss harvesting with Beancount, here’s what I set up:

1. Account Structure with Tax Classification

Assets:Investments:Fidelity:Taxable:VTI
Assets:Investments:Fidelity:IRA:Roth:VTI
Assets:Investments:Spouse:Vanguard:IRA:Traditional:VTSAX
Assets:Investments:Spouse:Company401k:VanguardTotalMarket

Notice the explicit account type in the hierarchy. This makes it immediately clear when querying whether a transaction is in a tax-advantaged account (where wash sales can be permanently disallowed).

2. Comprehensive Metadata Tags

Building on what Tina and Mike shared, here’s my complete metadata set:

For every purchase:

2026-01-15 * "Bought VTI - lot 2026-01"
  Assets:Investments:Fidelity:Taxable:VTI  100 VTI @ 215.50 USD
    ticker: "VTI"
    cusip: "922908769"
    acquisition_date: 2026-01-15
    cost_basis_per_share: 215.50 USD
    account_classification: "taxable"
    substantially_identical: "VTI,VTSAX,VITSX,ITOT,FSKAX"
  Assets:Investments:Fidelity:Taxable:Cash  -21550.00 USD

For every sale:

2026-11-20 * "Sold VTI at loss - harvesting" ^tax-loss-harvest
  Assets:Investments:Fidelity:Taxable:VTI  -100 VTI @ 195.50 USD
    ticker: "VTI"
    cusip: "922908769"
    acquisition_date: 2026-01-15
    original_cost_basis_per_share: 215.50 USD
    sale_price_per_share: 195.50 USD
    account_classification: "taxable"
    sale_type: "loss"
    realized_loss: 2000.00 USD
    wash_sale_checked: "2026-11-20"
    wash_sale_status: "clear"
  Assets:Investments:Fidelity:Taxable:Cash  19550.00 USD
  Income:Investments:CapitalLoss  -2000.00 USD

Key additions:

  • wash_sale_checked: Date I verified no wash sale violation
  • wash_sale_status: “clear”, “violation”, or “deferred”
  • original_cost_basis_per_share: Critical for Form 8949

3. Monthly Reconciliation Ritual

I have clients do this MONTHLY, not quarterly (even better than Mike’s excellent suggestion):

Step 1: Pull all taxable sales

SELECT 
  date,
  account,
  META('ticker') as ticker,
  META('sale_type') as sale_type,
  META('realized_loss') as loss,
  META('wash_sale_status') as status
WHERE 
  account ~ 'Taxable'
  AND META('sale_type') = 'loss'
  AND date >= [FIRST_DAY_OF_MONTH]
  AND date <= [LAST_DAY_OF_MONTH]

Step 2: For each loss sale, verify 61-day window

Check 30 days before and 30 days after for ANY purchase of substantially identical securities across ALL accounts (including spouse’s).

Step 3: Compare to broker 1099-B data

At year-end, validate your Beancount records against broker-provided 1099-B forms. Brokers report same-account wash sales in Box 1g. You should find:

  • All broker-reported wash sales are in your Beancount ledger
  • Any cross-account wash sales YOU identified are documented for Form 8949 adjustment

4. The “Invisible” Wash Sale Dangers

These trip up even experienced investors:

Dividend Reinvestment Plans (DRIPs)
If you have dividends auto-reinvesting, they can trigger wash sales. Check dividend payment dates:

2026-11-25 * "VTI dividend reinvested"
  Assets:Investments:Fidelity:Taxable:VTI  5.2 VTI @ 196.00 USD
    ticker: "VTI"
    acquisition_date: 2026-11-25
    substantially_identical: "VTI,VTSAX,VITSX,ITOT,FSKAX"
    source: "dividend_reinvestment"

If you sold VTI at a loss on November 20 and the dividend reinvested on November 25, that’s a wash sale.

Automated 401(k) Contributions
Your employer’s 401(k) contributions happen on a schedule. If your 401(k) target-date fund contains total market index exposure, selling total market funds at a loss in your taxable account within 30 days of a 401(k) contribution could be problematic.

Track your 401(k) contribution schedule:

; 401k contributions: every other Friday, auto-invested in Vanguard 2050 Target Date
; 2026-11-15, 2026-11-29, 2026-12-13, 2026-12-27

Spouse’s Separate Accounts
This is the #1 missed wash sale in my practice. Track your spouse’s automated investments and manually triggered purchases.

5. Year-End Wash Sale Audit

In January, before I prepare a client’s taxes, I run this comprehensive audit:

  1. Pull all loss sales for the tax year
  2. For each loss, check the 61-day window (even if it crosses into the next calendar year)
  3. Document wash sale adjustments:
    • Same-account (should match broker 1099-B)
    • Cross-account (your responsibility to report)
    • Cross-year (December sale + January purchase)
  4. Prepare Form 8949 worksheet with all adjustments
  5. Verify cost basis for replacement shares (wash sale loss gets added to basis)

6. The Calendar Days Rule (Critical Detail)

Tina mentioned this, but I want to emphasize: 30 calendar days means 30 calendar days.

If you sell on Friday, November 20, 2026:

  • 30 days before = October 21, 2026 (Tuesday)
  • 30 days after = December 20, 2026 (Saturday)

The 61-day window includes weekends, holidays, and market closure days. Do NOT count only trading days.

7. Form 8949 Preparation

When you have a cross-account wash sale that the broker didn’t report, here’s how to adjust on Form 8949:

Box checked: The broker’s 1099-B reported basis to the IRS
Column (e): Cost basis from 1099-B (without wash sale adjustment)
Column (g): Adjustment code “W” and the wash sale amount
Column (h): Adjusted gain/loss

Example:

  • You sold VTI for $19,550 (reported on Fidelity 1099-B)
  • Your cost basis was $21,550 (reported on Fidelity 1099-B)
  • Fidelity shows a $2,000 loss (they didn’t see the Vanguard purchase)
  • You bought VTSAX at Vanguard within 30 days (cross-account wash sale)
  • You disallow the $2,000 loss on Form 8949 with code “W”

Beancount should have all this data ready for your tax preparer (or for your own tax software).

8. My Offer to the Community

I’ve built Beancount templates for:

  • Tax lot tracking with proper metadata
  • Wash sale audit queries (monthly and year-end)
  • Form 8949 preparation worksheets
  • Substantially identical securities reference lists

If there’s interest, I’m happy to share these templates and work with Mike and Tina to create a comprehensive “Beancount for Tax-Loss Harvesting” toolkit.

The key insight: Beancount isn’t just for tracking your money—it’s your tax compliance documentation system. When you do it right, tax season becomes a query, not a crisis.

Great thread, everyone. This is exactly the kind of knowledge-sharing that makes the Beancount community valuable.