Skip to content

Documentation

Known limitations

Tax system

  • US taxes only. The tool models US federal income tax, Social Security, and Medicare. No support for other countries’ tax systems (VAT, PAYE, etc.).
  • Tax data not bundled. The tool does not ship with tax bracket data. Users provide bracket files via the taxes: config section, place them in ~/.config/hforecast/taxes/ for auto-discovery, or use flat rates (taxes: { effectiveRate: 0.30 }). Example bracket files for 2024 (federal, FICA, NY state, NYC local) are in examples/taxes/.
  • Inflation-projected brackets. For simulation years beyond the bracket file’s year, all bracket thresholds are auto-adjusted for inflation using the scenario’s inflationRate. This is a reasonable approximation for federal brackets (which the IRS indexes to CPI) but less accurate for state/city brackets (which are adjusted by legislature).
  • Social Security wage base uses CPI. The SS wage base is actually indexed by average wage growth (AWI), which historically exceeds CPI. The tool’s CPI-based projection will slightly under-grow the wage base, which slightly over-taxes wages near the cap in future years.
  • Married filing separately and head of household not supported. Only Single and Married Filing Jointly are modeled. Set filingStatus: "Married Filing Jointly" in the taxes: section of hforecast.yaml to use MFJ brackets.
  • City tax requires a bracket file or jurisdiction code. taxes.city must be a flat rate, a file path, or a jurisdiction code (e.g. NYC) that resolves to a local-nyc-YYYY.yaml file in the XDG taxes directory. Only cities with bracket data produce local tax.
  • Withholding falls back to actual brackets when no separate withholding file is provided. For state and local, if withholdingRates is omitted from the bracket file, actual rates are used for both withholding and annual calculations.
  • Cannot move states or cities mid-simulation. taxes.state and taxes.city are fixed for the entire plan.
  • No Alternative Minimum Tax (AMT).
  • No capital loss carryforward. Only the current-year $3,000 deduction is modeled. Losses cannot carry forward to future years.
  • No tax-loss harvesting.
  • Cost basis is unreliable for config-specified holdings. An @/@@ cost on a holding in the accounts: section is interpreted as the commodity’s starting price for the simulation, not as the historical price you paid, so it seeds no cost-basis lot. Capital gains on selling a holding that came from the config are therefore approximate — the tool has no record of what you actually paid. (When several holdings of one commodity carry different @/@@ prices, only the first is used as the starting price; the tool warns about the rest.)
  • No Roth conversion modeling. Use explicit transfer actions to model conversions.
  • Treasury interest is federal-only, but omitted from the Social Security provisional-income formula. A treasury coupon is taxed federally (and for NIIT) while being exempt from state and local tax, as in reality. However, it is not counted toward the combined income that decides how much of your Social Security is taxable, so having both treasury coupons and Social Security may slightly understate the taxable portion of benefits.
  • Equity dividends are taxed as ordinary income. Qualified-dividend (long-term capital gains) rates are not modeled.
  • No tax itemization.

Retirement accounts

  • RMDs use hardcoded US rules. Required minimum distributions from pre-tax accounts (Traditional 401k) are modeled with the IRS Uniform Lifetime table and start age baked into the tool. They are US-specific, do not reflect other jurisdictions, and the factors and start age must be updated in code as the law changes — the shipped table starts at age 72, whereas SECURE 2.0 moved the start age to 73 (rising to 75 in 2033). The distribution is also computed from the current balance rather than the prior-year-end balance.
  • Early-withdrawal penalty is hardcoded. A withdrawal from a Traditional 401k before the minimum retirement age incurs a flat 10% penalty (the US rule), baked into the tool as a constant. The rate is not configurable and there are no exceptions (e.g. Rule 72(t), first-home, medical). We’d eventually like to parameterize this.

Currency and geography

  • US-centric. No support for non-US tax systems or financial products. This is an area we’re exploring, likely through integration with existing codebases that can robustly handle these systems.
  • One location. Cannot move states or cities mid-simulation.
  • No cross-currency exchange rates. A currency in the currencies: section only takes an inflationRate and an interestRate; there is no way to price one currency in terms of another (only symbols: carry a price, and that price is denominated in the default currency). Each currency therefore accrues its own interest and inflation in isolation, and holdings in a non-default currency are never converted to the default currency. Multi-currency plans are best modeled in a single default currency.
  • Today’s-dollars valuation covers only the default currency. Each period the journal records a real-value price for the default currency under a TODAY<code> commodity (TODAYUSD, TODAYEUR, …, or TODAYCURR if unrecognized) — its inflation discount back to the plan start — so hledger --value=end,TODAYUSD restates the results in today’s money. Holdings denominated in a non-default currency are not discounted this way.
  • Real-value balances require --value=end, not --value=then. For balances and net worth (bal, balancesheet), value at the snapshot date with --value=end,TODAYUSD so a settled account reads zero. --value=then values each posting at its own date’s discount, which is right for flows (incomestatement) but leaves a residue on a balance-sheet account whose balance changed over time — e.g. a paid-off mortgage keeps a negative today’s-dollars balance equal to its real inflation gain. The nominal ($) postings are correct and net to zero; this is a valuation-mode choice, not a bookkeeping error.

Simulation mechanics

  • The funding cascade is not conditional. When the simulation is trying to find funds to pay for something, it tries to fund them from standard cash accounts (checking, savings), then to liquidate assets in a fixed order by account type. This order is automatic and not configurable, and it has no conditional logic at this stage. For example, you can’t say ‘pull from HSA only if the expense is a medical expense’ or ‘pull from IRA only if there is no penalty for doing so’.
  • Month-end date approximation. endOfPeriod Monthly is hardcoded to day 28, not the actual last day of the month. This slightly underestimates growth for months with 29-31 days.
  • Biweekly period incomplete. Using Biweekly as a period in annual mode causes a runtime error.
  • Linear change growth assumes 12 monthly deposits. The growth calculation for net account changes during a year assumes deposits/withdrawals are spread evenly across 12 months.
  • No portfolio rebalancing. Asset allocations are static; there is no automatic rebalancing to target percentages.
  • No conditional actions. Actions fire on fixed dates; they cannot be conditioned on account balances or other state.
  • Salary and general income growth is nominal, not inflation-plus. Salary and general income have their today’s-dollars base inflated up to the start date, but once started they escalate by growthRate alone — inflation is not layered on during the income’s active life. So growthRate is a nominal rate that must itself include any cost-of-living increase; a growthRate of 0 holds the income flat in nominal terms and lets its real value erode. This is asymmetric with expenses, which are inflation-adjusted throughout their life. (Social security is the exception — see below.)
  • Social security is inflation-indexed, approximating its COLA. A social-security income grows with the plan’s inflation rate every year it is in payment — a stand-in for the statutory cost-of-living adjustment — and its growthRate is ignored. Several real-world details are not modeled: the actual COLA is pegged to CPI-W specifically, not your general inflation assumption; the benefit before you claim is indexed to wage growth (which historically outpaces prices), so a future claim’s real value may be understated; benefits never fall in nominal terms (we rely on inflation being non-negative); and trust-fund solvency is assumed — the projected ~2030s shortfall, which could cut scheduled benefits by roughly a fifth absent legislation, is not applied.
  • Annual granularity is an approximation. Running with -g annual steps the simulation once per year for speed. Asset growth, dividends, coupons, and interest are applied once and dated at the end of the calendar year (so annual taxes capture them). A full year’s growth is applied even to a partial first year when the plan starts mid-year, and per-year compounding differs slightly from monthly compounding of the same annual rate.
  • Rate changes in annual mode take effect at period granularity. A rate changed mid-year by a manual action applies to growth, interest, and dividends within the same year (growth runs after actions in each period), but to inflation only from the following year (the inflation index is advanced at the start of each period, before actions fire).
  • No ETF fees, etc.

Journal inference

  • Per-account classification. With balance: infer-from-journal, every commodity in an account is classified the same way unless a symbols: entry or a per-commodity hforecast-asset-class: tag overrides it; a single account holding several asset classes should be split into subaccounts. A commodity with no explicit class defaults to equity (real-property in a property account); all such guesses are reported in one warning.
  • Priced holdings only. An inferred holding that hledger cannot value (no P directive and no @/@@ cost to infer a price from) is a hard error rather than a guess.
  • Coarse income/expense derivation. A query amount is the matched flow over one window (last-full-year or year-to-date) scaled to a full year — a rough run-rate, not a trend. Raw hledger period expressions, additional queryMethods, and simplifySymbols (collapsing tickers into synthetic per-class units) are not yet supported.

Loans and liabilities

  • A property’s cost basis comes from its recorded cost. Selling a property computes the gain against the commodity’s recorded cost (its @-cost lot) plus any capitalized property-improvement spending; a property with no recorded cost basis posts the full proceeds to long-term capital gains (see Cost basis is unreliable for config-specified holdings above).
  • Mortgage interest is not tax-deductible in the model. Loan-payment interest posts to an expenses account and the tax engine ignores it; there is no itemized mortgage-interest deduction.
  • No primary-residence capital-gains exclusion. A residence: true property is marked as such but the IRC §121 exclusion (up to $250k/$500k of gain on a primary home) is not yet applied — a home sale is taxed like any other long-term gain.
  • Loans require a rate and a term. A loan account amortizes from its current balance, its term (years), and interestRate — supplied as YAML fields or hforecast-loan-rate:/ hforecast-loan-term: tags (with an optional hforecast-loan-start: origination date that shortens the term to what remains). There is no support for a fixed payment: in place of a term, nor for interest-only, balloon, or refinanced loans; a loan missing its rate/term is reported in one warning and defaults to 0% over 30 years.
  • Liabilities are outside the funding cascade. The tool never borrows to cover a shortfall: a loan is a scheduled payment, not a source of funds.