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 inexamples/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 thetaxes:section ofhforecast.yamlto use MFJ brackets. - City tax requires a bracket file or jurisdiction code.
taxes.citymust be a flat rate, a file path, or a jurisdiction code (e.g.NYC) that resolves to alocal-nyc-YYYY.yamlfile 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
withholdingRatesis omitted from the bracket file, actual rates are used for both withholding and annual calculations. - Cannot move states or cities mid-simulation.
taxes.stateandtaxes.cityare 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 theaccounts: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 aninflationRateand aninterestRate; there is no way to price one currency in terms of another (onlysymbols:carry aprice, 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, …, orTODAYCURRif unrecognized) — its inflation discount back to the plan start — sohledger --value=end,TODAYUSDrestates 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,TODAYUSDso a settled account reads zero.--value=thenvalues 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 Monthlyis 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
Biweeklyas 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
growthRatealone — inflation is not layered on during the income’s active life. SogrowthRateis a nominal rate that must itself include any cost-of-living increase; agrowthRateof 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-securityincome grows with the plan’s inflation rate every year it is in payment — a stand-in for the statutory cost-of-living adjustment — and itsgrowthRateis 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 annualsteps 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 asymbols:entry or a per-commodityhforecast-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 apropertyaccount); all such guesses are reported in one warning. - Priced holdings only. An inferred holding that hledger cannot value (no
Pdirective and no@/@@cost to infer a price from) is a hard error rather than a guess. - Coarse income/expense derivation. A
queryamount is the matched flow over one window (last-full-yearoryear-to-date) scaled to a full year — a rough run-rate, not a trend. Raw hledger period expressions, additionalqueryMethods, andsimplifySymbols(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 capitalizedproperty-improvementspending; 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: trueproperty 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
loanaccount amortizes from its current balance, itsterm(years), andinterestRate— supplied as YAML fields orhforecast-loan-rate:/hforecast-loan-term:tags (with an optionalhforecast-loan-start:origination date that shortens the term to what remains). There is no support for a fixedpayment: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.