Documentation
Taxes
Hforecast models US income tax: federal, state, city/local, and FICA (Social Security + Medicare), plus capital gains on investment and property sales. Tax is withheld as income is received and reconciled once a year (see how the simulation works). This page covers how to configure it.
Tax modeling here is rough and for planning only — it is not a substitute for a tax professional. See Known limitations for the caveats (no AMT, no capital-loss carryforward, approximate FICA wage-base handling, and more).
The quick way: a flat effective rate
The simplest configuration takes a single flat percentage of income each year and skips brackets entirely:
taxes:
effectiveRate: 0.20 # a flat 20% of income
effectiveRate overrides every per-jurisdiction setting below. It’s ideal for a first pass.
Per-jurisdiction configuration
For more realism, set each jurisdiction independently. taxes has these fields, all optional:
taxes:
filingStatus: single # single | married-filing-jointly (default: single)
federal: examples/taxes/federal-2024.yaml # a bracket file
state: ny # a jurisdiction code
city: nyc # a jurisdiction code
fica: 0.0765 # a flat rate
Each of federal, state, city, and fica accepts one of three forms:
- A flat rate — a number, e.g.
federal: 0.22, applied to all of that jurisdiction’s income. - A bracket file — a path to a YAML file, e.g.
federal: examples/taxes/federal-2024.yaml. Relative paths are searched in the current directory, then in the config directory (below). - A jurisdiction code — a short string like
nyornyc, resolved to a bundled bracket file.
How each jurisdiction resolves differs, because some are national and some aren’t:
| Field | Unset | Flat rate | File path | Jurisdiction code |
|---|---|---|---|---|
federal |
auto-discovers the newest federal-*.yaml |
✓ | ✓ | rejected (national tax) |
fica |
auto-discovers the newest fica-*.yaml |
✓ | ✓ | rejected (national tax) |
state |
no state tax | ✓ | ✓ | state-<code>-*.yaml, else the all-states dataset |
city |
no local tax | ✓ | ✓ | local-<code>-*.yaml |
For state, a jurisdiction code first looks for a hand-curated state-<code>-*.yaml (e.g.
state-ny-2024.yaml); if none is bundled, it falls back to the all-states JSON dataset — lower
fidelity, but it covers every state. For city, there is no dataset fallback, so an unknown city code
is an error. federal and fica are national, so a jurisdiction code there is rejected outright.
Where tax files live
At run time hforecast looks for bracket files in your config directory:
${XDG_CONFIG_HOME:-~/.config}/hforecast/taxes/
Install the bundled files (federal, FICA, a sample state and city) and download the all-states dataset there with:
make install-taxes # or: ./scripts/install-taxes.sh
make install-taxes ARGS=--force # overwrite existing files
This copies the YAMLs from examples/taxes/ and downloads the state income-tax dataset (from LevyIO,
CC BY 4.0) into the directory above, where hforecast auto-discovers them. You can also point at a file
directly with a path in the config, bypassing the directory entirely. See Installation.
Writing your own bracket file
A bracket file is plain YAML. Federal is the richest; the bundled examples/taxes/federal-2024.yaml
is the reference. The shape:
year: 2024
standardDeduction:
single: 13850
married: 27700
# Progressive brackets: each entry taxes income up to maxValue at marginalRate.
rates:
single:
- maxValue: 11600
marginalRate: 0.10
- maxValue: 47150
marginalRate: 0.12
# ...
married:
- maxValue: 23200
marginalRate: 0.10
# ...
# Long-term capital-gains brackets (flatRate per band).
capitalGains:
single:
- maxValue: 44625
flatRate: 0
- maxValue: 492300
flatRate: 0.15
- maxValue: 10000000
flatRate: 0.20
# Net Investment Income Tax thresholds.
niit:
single:
- maxValue: 200000
flatRate: 0
- maxValue: 10000000
flatRate: 0.038
withholdingRates: { single: [...], married: [...] } # paycheck withholding tables
supplementalRate: 0.22 # e.g. bonus withholding
State and city files use the same rates/standardDeduction shape with less detail. Bracket
thresholds are keyed by single/married, matching filingStatus. When a plan runs past the newest
file’s year, thresholds are inflation-adjusted forward automatically.
How and when tax is applied
Withholding comes out of each paycheck as income is received. Once a year, on January 1, hforecast files
for the prior calendar year: it totals that year’s liability across all configured jurisdictions, nets
it against what was withheld, and posts the settlement dated December 31 of that year. The result lands
in expenses:taxes:* accounts you can query directly:
hledger -f hforecast.journal bal expenses:taxes -Y
See Analyzing output for more queries and How it works for the reconciliation timing.
How each kind of income is taxed
- Wages — federal, state, city, and FICA (Social Security up to the wage base, plus Medicare).
- Bond coupons and cash interest — ordinary income at every level (federal, state, city).
- Treasury coupons — federal only. US Treasury interest is exempt from state and local income
tax, so a treasury’s coupon feeds the federal ordinary-income base (and the federal Net Investment
Income Tax) but never the state or city base. This is the one difference between a
bondand atreasuryin the model — they are otherwise identical. - Equity dividends — ordinary income (a v1 simplification; qualified-dividend rates aren’t modeled).
- Realized capital gains — long-term at the flat capital-gains rate; short-term as ordinary income.
- Social Security benefits — the taxable portion follows the IRS provisional-income formula.
Anything held in a tax-advantaged account (401k, Roth, HSA) has its coupons, interest, and dividends routed to a deferred income account, so they aren’t taxed as they accrue.
See also
- Configuration — the
taxesblock in the full reference - Known limitations — what the tax model does and doesn’t cover
- Installation — installing the bundled tax data