Bond Ladders: Tracking Maturity Dates and Reinvestment

I’ve built a 5-year Treasury bond ladder and finding Beancount tracking to be both powerful and tricky.

My Ladder Structure

  • Year 1: USD 10,000 in 1-year Treasury
  • Year 2: USD 10,000 in 2-year Treasury
  • Year 3: USD 10,000 in 3-year Treasury
  • Year 4: USD 10,000 in 4-year Treasury
  • Year 5: USD 10,000 in 5-year Treasury

Each year, the maturing bond gets reinvested at the 5-year rate.

Recording Bond Purchases

2026-01-15 * "Buy 5-year Treasury"
  Assets:Investments:Bonds:Treasury5Y  10,000 TREAS5Y {1.00 USD}
  Assets:Bank:Checking                  -10,000.00 USD
  maturity-date: 2031-01-15
  coupon-rate: 4.25%

The Maturity Challenge

When a bond matures, I need to:

  1. Record the principal return
  2. Record final interest payment
  3. Remove the bond position
  4. Buy the new 5-year bond

Questions

  1. How do you query for upcoming maturities?
  2. Anyone track accrued interest for bonds bought between coupon dates?
  3. Tips for managing the reinvestment workflow?

For querying upcoming maturities, I use a Python script that parses metadata:

from beancount import loader
from datetime import date, timedelta

entries, errors, options = loader.load_file('main.beancount')

upcoming = []
for entry in entries:
    if hasattr(entry, 'meta') and 'maturity-date' in entry.meta:
        mat_date = entry.meta['maturity-date']
        if mat_date <= date.today() + timedelta(days=90):
            upcoming.append((mat_date, entry))

for mat_date, entry in sorted(upcoming):
    print(f"{mat_date}: {entry.narration}")

Recording Maturity

Here’s my full maturity workflow:

; Bond matures
2027-01-15 * "Treasury 1Y matures"
  Assets:Investments:Bonds:Treasury1Y  -10,000 TREAS1Y {1.00 USD}
  Assets:Bank:Checking                  10,000.00 USD  ; Principal
  Income:Interest:Treasury               -200.00 USD   ; Final coupon

; Reinvest at the long end
2027-01-15 * "Buy 5-year Treasury (reinvestment)"
  Assets:Investments:Bonds:Treasury5Y  10,000 TREAS5Y {1.00 USD}
  Assets:Bank:Checking                  -10,000.00 USD
  maturity-date: 2032-01-15
  coupon-rate: 4.50%
  reinvested-from: "TREAS1Y-2026"

The reinvested-from metadata creates an audit trail.

On the accrued interest question—this trips up a lot of people.

The Problem

If you buy a bond between coupon dates, you pay the seller for interest they’ve earned but haven’t received yet. This is accrued interest.

Example: Bond pays semi-annual coupon Jan 15 and Jul 15. You buy Mar 15.

  • Seller earned 2 months of interest (Jan 15 - Mar 15)
  • You pay them that accrued interest
  • On Jul 15, you receive the full 6-month coupon

Recording Accrued Interest

2026-03-15 * "Buy Treasury mid-coupon"
  Assets:Investments:Bonds:Treasury  10,000 TREAS {1.00 USD}
  Assets:Bank:Checking               -10,100.00 USD
  Income:Interest:AccruedPaid            100.00 USD  ; Accrued interest paid to seller

2026-07-15 * "Receive semi-annual coupon"
  Assets:Bank:Checking                    200.00 USD  ; Full 6-month coupon
  Income:Interest:Treasury               -200.00 USD

Net Interest Income

Your actual interest income is the net:

  • Received: USD 200
  • Paid (accrued): USD 100
  • Net: USD 100 (4 months of ownership)

Query to see true interest income:

SELECT sum(number(position))
WHERE account ~ 'Income:Interest'

I want to add a perspective on why bond ladders rather than bond funds.

Bond Ladder vs Bond Fund

Bond fund (like BND):

  • No maturity date
  • NAV fluctuates with interest rates
  • You might sell at a loss

Individual bonds (ladder):

  • Known maturity date
  • Hold to maturity = get your principal back
  • Interest rate changes don’t affect you if you hold

When Ladders Make Sense

  1. Retirement income: Known cash flows on specific dates
  2. Liability matching: Save for a car in 3 years, buy a 3-year bond
  3. Interest rate uncertainty: Lock in rates without NAV risk

My Dashboard Query

I run this to see my ladder at a glance:

SELECT 
  currency,
  meta("maturity-date") AS maturity,
  meta("coupon-rate") AS rate,
  sum(cost(position)) AS principal
WHERE account ~ 'Assets:Investments:Bonds'
GROUP BY currency, maturity, rate
ORDER BY maturity

This shows me exactly when each rung matures and at what rate.