Cross-border Household Investment & Retirement Planner — Canada/US dual-citizen finance & retirement optimization on ledger-cli

1 branch

README.md

Chirp

Cross-border Household Investment & Retirement Planner — cross-border personal-finance consolidation and tax-aware reporting for dual Canada/US citizens, built on top of ledger-cli.

Chirp is inspired by Owl (a US retirement optimizer), but aimed at the dual-citizen problem: you have accounts in both countries and two tax systems apply at once — the US taxes you as a citizen on worldwide income while Canada taxes you as a resident, reconciled through the Canada–US treaty and foreign tax credits.

Your ledger journal is the single source of truth. Chirp shells out to the ledger binary to parse it and apply FX valuation, then adds a cross-border tax brain on top: it consolidates CAD + USD holdings into one net-worth view, classifies every account by its cross-border tax treatment, and flags the reporting regimes you may trip (FBAR, Form 8938, CRA T1135, PFIC, foreign-trust 3520 risk).

Not tax advice. The classifications and thresholds are informational pointers to the relevant regimes, not professional guidance.

Requirements

  • SBCL + ocicl (dependencies: clingon, version-string, alexandria, cl-ppcre, cambl)
  • ledger 3.x on your PATH
  • lp_solve on your PATH (only for chirp plan/optimize)

Build & test

make            # builds ./chirp
make test       # runs the FiveAM suite (asdf:test-system :chirp/tests)

Usage

Point chirp at a journal with -f or the LEDGER_FILE environment variable. If your commodity/FX prices live in a separate ledger price database rather than inline in the journal, pass it with --price-db FILE (or set LEDGER_PRICE_DB); chirp threads it into every ledger call so valuations use those prices.

# Consolidated net worth + reporting-threshold flags (valued in USD)
chirp networth -f examples/dual-citizen.ledger

# Value everything in Canadian dollars instead
chirp networth -X CAD -f examples/dual-citizen.ledger

# Value each holding at the FX rate on its own date (cost-basis view)
chirp networth --historical -f examples/dual-citizen.ledger

# FBAR (FinCEN 114): maximum value of each foreign account during the year
chirp fbar --year 2025 -f examples/dual-citizen.ledger

# CRA T1135: specified foreign property on a cost basis (max cost over the year)
chirp t1135 --year 2025 -f examples/dual-citizen.ledger

# Inspect how an account is classified
chirp classify Assets:CA:TFSA:XIU
chirp classify -f examples/dual-citizen.ledger   # every account in the journal

# Dual-currency realized capital gains (CAD for CRA, USD for IRS)
chirp gains -f main.ledger
chirp gains --year 2026 -f main.ledger

# Tax-year income summary by type and source, dual-currency (FTC groundwork)
chirp income --year 2026 -f main.ledger

# Income tax estimate, US (federal+state) and Canada (federal+provincial)
chirp tax --year 2026 --filing mfj --province ON -f main.ledger

# Foreign tax credit position (US return, Form 1116) with treaty re-sourcing
chirp ftc --year 2026 -f main.ledger                    # US tax auto-estimated
chirp ftc --year 2026 --us-tax 42000 --resource -f main.ledger
chirp ftc -f main.ledger                                # multi-year carryover schedule

# Tax-minimising withdrawal-source mix for an after-tax spending target
chirp optimize --spend 80000 --province ON -f main.ledger

# Multi-year drawdown plan: conversions, benefits, and claim-age optimization
chirp plan --spend 60000 --years 15 --province ON -f main.ledger
chirp plan --spend 60000 --province ON --current-age 60 \
           --cpp 15000 --oas 8500 --ss 24000 --optimize-claiming -f main.ledger

# Monte-Carlo stress test with per-account asset allocation
chirp plan --spend 55000 --province ON --equity 0.6 --montecarlo 2000 -f main.ledger

# Print a guide teaching a coding agent how to drive chirp
chirp skill

# Run as an MCP server (stdio) exposing the reports above as tools
chirp mcp

For coding agents (chirp skill and chirp mcp)

chirp skill prints a self-contained guide — the journal annotations (class/country/owner/use), the named-commodity convention for real estate, Owner: profile directives for mixed households, and what each command answers. Point an agent at it first.

chirp mcp runs a Model Context Protocol server over stdio, exposing skill, networth, fbar, t1135, income, gains, tax, ftc, optimize, and plan as tools an agent can call against a journal. Register it with any MCP client, e.g. Claude Code:

claude mcp add chirp -- chirp mcp

Multi-year drawdown plan

chirp plan extends the optimizer across N years: it chooses each year's withdrawals so spending is met every year and discounted lifetime tax is minimised, including a terminal tax on ordinary (RRSP/401k) balance left at the horizon. That terminal term is what produces the classic result — drain the tax-deferred accounts at moderate brackets over time rather than leave them to a ~50% estate tax, while preserving tax-free and capital-gains balances:

  yr        tax-free     cap-gains      ordinary           tax   ord-bal-end
   0            0.00          0.00     78,291.62     18,291.61  1,000,859.60
   5            0.00          0.00     78,291.62     18,291.62    732,139.75
  14            0.00          0.00     78,291.62     18,291.62    136,042.17

The plan works in real (inflation-adjusted) dollars, so with fully inflation-indexed brackets no per-year bracket adjustment is needed. To stress under-indexation (bracket freezes, chained-CPI lag), pass --bracket-drift (e.g. 0.01): brackets then erode in real terms each year, so a fixed real drawdown pays progressively more tax. The plan also models the OAS clawback band so it avoids pushing income into the 15% recovery-tax zone.

It also models Roth/RRSP conversions (ordinary → tax-free, taxed now — the optimizer fills cheap brackets to escape the estate tax) and CPP / OAS / US Social Security income streams, which the treaty taxes only in Canada, so they raise the Canadian bracket baseline and reduce the withdrawal need. With --optimize-claiming it enumerates claim ages and recommends the ones leaving the most after-tax terminal wealth (the pre-benefit gap years become prime conversion years):

Recommended claim ages:  CPP at 65 · OAS at 65 · US Social Security at 67

  yr       benefit    tax-free   cap-gains    ordinary     convert        tax
   0          0.00        0.00        0.00   86,038.32   19,736.68  26,038.34
   5     23,500.00        0.00        0.00   62,316.74   28,933.25  24,168.07
  10     47,500.00        0.00        0.00   38,316.74   28,933.26  19,356.07

It also models required minimum distributions (a RRIF-schedule floor forcing ordinary distributions from age 72 — with --current-age — whose forced excess is auto-converted to tax-free) and mortality (--mortality survival-weights the objective from a mortality table, so far-future consequences count less). Claim ages are chosen either by enumeration (--optimize-claiming, accurate) or a single MILP with binary claim variables (--milp-claiming, faster) — both agree on the sample.

With --equity (per bucket via --equity-tf/-cg/-ord) it sets each bucket's expected return from a stock/bond blend, so the optimizer holds growth where it's tax-sheltered; and --montecarlo N solves the plan at expected returns, then replays that fixed strategy over N random return sequences to report the probability spending is met every year (sequence-of-returns risk) and the terminal-wealth distribution:

Monte-Carlo stress test  (2000 sims, 60% equity)
  Probability spending is met all years   94.5 %
  after-tax terminal wealth (real):
    10th pct         537,394.28
    median           992,093.17
    90th pct       1,740,971.40

Modelled as a linear program the same way Owl models retirement, solved with lp_solve (a robust MILP solver) at full bracket resolution over a 30-year horizon. A directional planning aid, not a financial plan.

Withdrawal-source optimizer

chirp optimize --spend N answers "which accounts should I draw from this year?" It chooses how much to withdraw from each of three buckets — tax-free (TFSA/Roth), capital-gains (non-registered securities), and ordinary (RRSP/RRIF/401k/IRA) — to hit an after-tax target while minimising combined CA+US tax, solved as a linear program (the same LP/MILP approach Owl uses).

The cross-border insight that makes it a clean LP: under the foreign tax credit, the combined marginal rate on an ordinary dollar is max(CA, US-federal) + US state, and on a realized capital-gains dollar it's max(50%×CA, US-LTCG) — both non-decreasing, so the tax is convex and the solver fills the cheapest sources first (tax-free → capital-gains → ordinary). The capital-gains bucket's embedded-gain fraction is computed from the ledger's cost basis (via chirp's ACB engine), or set with --gain-fraction. First slice: single year, fixed FX, capital-gains taxed at one blended rate. Planning aid, not advice.

Tax estimate

chirp tax estimates income tax for both countries from the income engine: US federal + state (--state, default none) and Canada federal + provincial (--province), 2025 brackets, standard deduction / basic personal amount, and Canada's 50% capital-gains inclusion. Sub-national brackets live in src/tax-data.lisp — an Owl-style data table you edit in one place (Canadian provinces plus a starter set of US states; no-tax states are zero brackets). A US citizen resident in Canada usually owes no state tax, so --state defaults to none; --province is the one that matters.

US long-term capital gains (held >1yr) are taxed at the preferential 0/15/20% rates, stacked on ordinary income. Still simplified — no NIIT/AMT, no provincial surtaxes or Quebec abatement, approximate brackets — a planning estimate, not a return. Its other job is to feed chirp ftc: without --us-tax, the FTC limit uses the computed US federal tax before credits.

Foreign tax credit

chirp ftc assembles the US-return foreign-tax-credit position from the ledger: income by Form 1116 basket (passive / general) and source, and the Canadian income tax actually paid. Given --us-tax (US tax before credits) it computes the per-basket limitation and the creditable amount, with any excess as carryover. --resource applies the treaty (Article XXIV) re-sourcing of US-source income to Canada — which, for a US citizen resident in Canada, is often what rescues an otherwise-stranded credit:

Form 1116 limitation (statutory source):     |  (with treaty resourcing):
  General      fgn tax 0.00   creditable 0.00 |    General   creditable 2,084.08
  Passive      fgn tax 2,520  creditable 2,076 |    Passive   creditable   435.92
  carryover (excess)              444.19       |    carryover                 0.00

Run chirp ftc without --year for a multi-year carryover schedule: each year's foreign tax is credited first up to the limit, and any excess carries forward (per basket) to fill unused limit in later years.

Multi-year foreign tax credit carryover (treaty resourcing)
  year          fgn tax       US tax     creditable     carryforward
  2024        45,880.00    17,663.00      17,663.00        28,217.00
  2025         2,920.00    18,047.00      18,047.00        13,090.00
  2026         2,160.00    15,814.00      15,250.00             0.00

The credit computation is a planning aid with stated simplifications (passive/general baskets only, tax apportioned by income share, 10-year carryforward with carryback not modeled), not Form 1116 itself.

Income summary

chirp income reads your Income accounts, classifies each item by type (employment / interest / dividend / pension / …) and source country, and values it in both currencies at the transaction-date FX. Capital gains come from the inventory engine (the correct dual-currency figure), not from Income:CapitalGains postings. It then splits income US-source vs foreign-source — the split that foreign-tax-credit reconciliation rests on (US taxes you as a citizen, Canada as a resident; each credits the other's tax on the overlapping income).

Dual-currency capital gains

A US citizen resident in Canada reports the same disposal to two authorities, in two currencies, using two different cost methods:

  • CRA — gain in CAD, cost as Adjusted Cost Base (a single running average across all units of identical property);
  • IRS — gain in USD, cost by FIFO lots, each struck at the acquisition-date exchange rate.

So the same shares carry two different cost bases, and because FX moves between buy and sell, the two gains diverge — a trade can be a gain in one currency and a loss in the other. chirp gains computes both, and flags whether each side is currently taxable given the account type:

2026-05-01  Assets:CA:TFSA:XIU
  sold 80 XIU  (FIFO from 2021-03-01)   [PFIC — Form 8621]
    CAD (CRA, ACB):   proceeds     3,200.00 - cost     2,506.67 = gain       693.33   [sheltered]
    USD (IRS, FIFO):  proceeds     2,336.00 - cost     1,896.00 = gain       440.00   [taxable]

Here the TFSA disposal is sheltered for the CRA (tax-free in Canada) but taxable for the IRS and a PFIC — the classic cross-border trap.

chirp needs no lot annotations: it reads every buy and sell, asks ledger to value each at its transaction-date FX in both currencies, and matches lots automatically (ACB pool for CAD, FIFO queue for USD). A plain sale is enough:

2026-05-01 * Sell 80 XIU
    Assets:CA:TFSA:XIU   -80 XIU @ CAD 40.00
    Assets:CA:TFSA:Cash   CAD 3,200.00
    Income:CapitalGains

Selling more units than were held is reported with a warning. The CAD/ACB basis pools identical property across all your non-registered accounts (per owner), as the CRA requires; registered plans are kept separate, and the USD/FIFO basis stays per account, as the IRS applies it.

How valuation works

Chirp asks ledger for each posting's raw quantity plus a value computed with market(amount, DATE, 'BASE'):

  • current mode (default) values every holding at today's FX/price — the right basis for net worth;
  • --historical values each posting at the rate on its own date — the right basis for cost basis, capital-gain/FX-gain, and FBAR maximums.

Multi-currency and securities chain automatically (e.g. a TSX ETF priced in CAD is converted ETF → CAD → USD) using the P price directives in your journal.

Monetary amounts are represented with cambl (Commoditized Amounts and Balances, from the ledger ecosystem): every amount is a quantity bound to a commodity, with exact rational arithmetic and display rounding, so an averaged cost like 7520/3 stays exact internally and prints as 2,506.67. src/money.lisp is chirp's thin layer over it.

Account classification

Ledger account names are user-defined, so chirp classifies each account two ways, explicit annotation first, name convention as fallback.

Annotating accounts (recommended)

Tag the account on its ledger account directive. These chirp tags live in your journal (single source of truth) and are ignored by ledger's own reports, so they don't affect anything else you do with the file:

account Assets:CA:Investing
    note Self-directed TFSA at Questrade
    ; class:   tfsa       ; overrides name convention
    ; country: ca         ; ca | us
    ; owner:   anthony    ; free-form; for per-person reporting later
  • class: — one of the class keys below (rrsp, rrif, lira, tfsa, fhsa, resp, rdsp, ca-nonreg, ca-cash, 401k, traditional-ira, roth-ira, hsa, us-brokerage, us-cash, ca-realestate, us-realestate, liability).
  • country:ca or us (otherwise inferred from the class or path).
  • owner: — free-form (e.g. anthony, sam, joint). When present, networth adds a By owner section: each owner's net worth plus their own foreign-financial total, since FBAR/8938 are filed per US person.

Annotations follow include directives and are matched against the exact account name. Anything you don't annotate falls back to convention.

Note: ledger stores account-directive metadata but does not expose it to its own query/format engine, so chirp scans the journal for these directives itself.

Name convention (fallback)

When an account isn't annotated, chirp classifies by path segments (:RRSP, :401k, :TFSA, :RothIRA, :House…) via *classification-rules* in src/classify.lisp. Recognized classes include:

Canada US
RRSP, RRIF, LIRA (treaty pension deferral) 401(k), Traditional IRA
TFSA, FHSA, RESP, RDSP (foreign-trust / PFIC risk to the US) Roth IRA (treaty election keeps it tax-free in Canada), HSA
Non-registered brokerage (PFIC exposure), chequing/savings Taxable brokerage, checking/savings
Real estate (non-financial) Real estate (non-financial)

Simplifications & known limitations

chirp is a planning estimator, not a tax preparer. The engine is validated by worked examples hand-derived from the published 2025 schedules (see the "Reference cases" in tests/tests.lisp) — but those are not reconciled against a filed return or a commercial calculator, which would be the next validation step.

Modelled (with cited tests): 2025 federal + provincial/state brackets (the Canadian 14.5% blended low rate, Bill C-4); the federal BPA phase-out, Quebec abatement, and Ontario surtax; US long-term capital-gains rates; character-aware capital-loss carryforward (short- vs long-term, §1211/§1212 $3,000 limit only when there's income to absorb it); NIIT; the §988 mortgage gain (personal-loss nondeductibility + $200 de minimis); §121/PRE real estate; the OAS clawback (estimate, single-year optimizer, and the multi-year plan); treaty Art. XVIII residence-sourcing of CPP/OAS/SS; the FTC (owner-scoped income and Canadian tax paid); and cost-basis T1135 / max-value FBAR.

Remaining simplifications, so you know where the estimate is still approximate:

  • No AMT — neither the US AMT nor the revamped (2024) Canadian AMT, which can bite large capital-gains realizations.
  • US Social Security is treaty-sourced to Canada but included in full — the 85%-inclusion rule (15% treaty-exempt in Canada) is not modelled.
  • The optimizer's marginal schedule models ordinary brackets, the surtax, and the OAS clawback; it does not include NIIT or the §121/PRE exemptions (those live in the estimate only).
  • FTC models the passive + general baskets only, with 10-year carryforward and no carryback; the capital-loss long-term slice isn't owner-filtered (a multi-owner edge case).
  • §988 uses pooled-average basis (not per-tranche); the FBAR maximum for securities accounts is an estimate (year-end prices on each date's holdings — use your statement's high value).
  • 2025 brackets only; the plan is a real-dollar model (indexed brackets need no adjustment), and --bracket-drift stress-tests under-indexation.

License

Copyright (C) 2026 Anthony Green.

chirp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the LICENSE file. Distributed WITHOUT ANY WARRANTY.