Skip to content

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 ny or nyc, 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 couponsfederal 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 bond and a treasury in 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