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, abalance: infer-from-journalor an income/expensequeryreads 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-balancefills an account to a target,contribute-continuouslyadds a fixed amount, andcontribute-remaindersweeps whatever is left. Cash routed to an account that has aninvest: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
- Granularity — how the step length changes the run
- The general account — the operating hub in detail
- Actions — the one-off events in phase 4
- Known limitations — the approximations behind this model