Skip to content

Documentation

How the simulation works

Hforecast turns a declarative plan into a ledger by stepping through time one period at a time. This page explains the mental model: what a period is, what happens inside one (and in what order), how values resolve, and where money goes when an account can’t cover a cost. You don’t need any of this to write a config — but it explains why the output looks the way it does.

The plan and the period

A run has a start (today, or your birthday-derived age) and an end (endOfPlanAge). Hforecast walks from start to end in fixed steps whose length is the granularity: one day, one month (the default), or one year. Each step advances the clock, records transactions and price directives to the journal, and carries balances forward to the next step.

Everything that happens in a period shares the period’s start date. A monthly step posts on the 1st; an annual step posts on January 1. This matters for actions: an action is placed in the period its date falls in, but its transaction is stamped with that period’s start date, not the exact date you wrote. (Use a finer granularity when you need actions dated precisely.) The reason is that a period holds a single snapshot of state, so same-period events must share a date to see one another — a mortgage taken out this period has to be visible to this period’s loan payment.

What happens in one period

Within each period, hforecast runs a fixed sequence. The order is deliberate — each phase sets up the next:

1. Inflation      advance the inflation index for the period
2. Income         salary and Social Security land in the general account;
                  pre-tax 401(k)/HSA diversions and withholding come out first
3. Expenses       recurring bills are paid from the general account
4. Actions        one-off events, income rules, annual taxes, RMDs — in date order
5. Loan payments  every loan amortizes one period (interest + principal)
6. Auto-invest    each account sweeps its leftover cash into holdings
7. Close          grow asset prices, mark to market, pay dividends/coupons/interest

The guiding idea: income lands first so allocations and taxes can see it, bills go out next, then the remaining actions run (income rules allocate the true remainder, one-off events fire), loans amortize (so a mortgage that originated this period pays its first installment), and finally the auto-invest sweep puts whatever cash is left to work — after income-rule priorities and loan payments have taken their share. The period closes by growing assets, recording their new prices, and distributing that period’s investment income.

See the general account for how cash flows through the hub in the middle of all this, and income rules and funding below.

The general account

Nearly all cash moves through one operating account, assets:general. Income is deposited there; expenses, taxes, and investments are paid from there; income rules allocate its balance to your other accounts. It’s a single real account, created automatically — not a concept sprinkled across the ledger. The general account covers it in full.

How values resolve

Most numbers in a plan have several possible sources, resolved in a fixed precedence. For any given field, hforecast uses the most specific value available and falls back outward:

  • Explicit — a value you wrote in the config wins.
  • Journal — with -f, a balance: infer-from-journal or an income/expense query reads the number from your journal. See Journal inference.
  • Global — rates default from your globals: block (inflation, per-class growth and yield).
  • Built-in default — anything still unset takes a sensible built-in (inflation 2.5%, equity growth 7%, and so on).

Growth is a property of an asset class (equity, bond, treasury, cash, real property), not of an account. Set it globally in globals: or per ticker in symbols:; there is no per-account growth rate. Run with --explain to see every resolved value tagged with where it came from ((journal), (global), (default), or untagged when explicit).

How amounts are inflated and grown

Every currency amount you write in the config is in today’s (plan-start) dollars. You never write future, inflated figures. Hforecast inflation-adjusts each amount from the plan start to the date it takes effect — so a $5,000 expense you write today costs the inflated equivalent when it’s actually charged years out. What happens after that first adjustment is where the important distinction lies: some amounts keep pace with inflation for the rest of their life, and some don’t.

What you write Inflated to… Then, over its life, it…
A recurring expense (expenses:) its date, re-inflated every period keeps pace with inflation
A mortgage / loan payment fixed when the loan originates stays fixed in nominal dollars (does not grow)
A one-off action amount — spend-money, transfer-money, reimburse, purchase-stock-by-amount, make-additional-loan-payment, receive-income the date it fires — (it happens once)
A salary or general income amount its start date grows by its growthRate, not inflation
A Social Security benefit amount its date, re-inflated every period keeps pace with inflation (COLA; growthRate not allowed)
401(k) / HSA contributions, employer match, and HSA seed its date, re-inflated every period keeps pace with inflation

The one that surprises people is ordinary income. A salary (or general) income’s amount is honored in today’s dollars at the age it starts — a salary beginning at 40 has its stated purchasing power in the year you turn 40 — but from there it follows growthRate, a nominal rate, and inflation is not added on top. So growthRate must itself include any cost-of-living increase you want: leaving it at 0 holds the income flat in nominal dollars, which loses real value every year. This is deliberately different from expenses, which re-inflate every period.

Two kinds of income-side amounts are exceptions that do track inflation: contributions to retirement and HSA accounts (mirroring how the IRS indexes those limits), and Social Security benefits, which follow a cost-of-living adjustment. Because a Social Security benefit’s growth is fixed to that COLA, setting growthRate on a social-security income is rejected as a config error.

Income rules and the funding cascade

Two mechanisms move money automatically:

  • Income rules (incomeRules:) run each period, in list order, allocating the general account’s cash to your other accounts: build-target-balance fills an account to a target, contribute-continuously adds a fixed amount, and contribute-remainder sweeps whatever is left. Cash routed to an account that has an invest: allocation is then invested by the auto-invest sweep.
  • The funding cascade runs in the other direction. When the general account can’t cover an expense, tax bill, or loan payment, hforecast liquidates other accounts to raise the cash — automatically, in a fixed order by account type: savings → brokerage → HSA → Roth IRA → traditional 401(k) → property, and within an account in a safest-first order (respecting an account’s sellOrder:). Retirement and HSA withdrawals incur the appropriate taxes and early-withdrawal penalties; taxable holdings incur capital-gains tax.

Taxes over a year

Income tax is withheld as income is received (like a paycheck) and then reconciled once a year. On January 1, hforecast files for the prior calendar year: it computes the full year’s liability, nets it against what was already withheld, and posts the settlement dated December 31 of that prior year — so a year’s true taxes sit in that year. See Taxes for what’s modeled and how to configure brackets.

The output

The result is a standard hledger journal, streamed as the run proceeds: transactions for every flow, plus price directives that value your holdings at each period’s end. Because it’s ordinary hledger, you analyze it with ordinary hledger commands — net worth, cash flow, tax burden, and today’s-dollars valuation. See Analyzing output.

See also