Skip to content

Documentation

hforecast.yaml reference

The hforecast.yaml file is the single configuration file for Hforecast. It defines your financial scenario declaratively: who you are, what you earn, what you spend, what you own, and how you want funds distributed.

Every section is optional except your age. See full.yaml for a worked example that exercises nearly every field, and defaults.yaml for the fallback values.

Top-level fields

age: 35                    # Your current age (whole years)
# birthday: 1990-06        # ...or a birth year-and-month, instead of age (more precise)
endOfPlanAge: 90           # Age the simulation stops at, exclusive (optional; default: 10 years out)

Specify exactly one of age or birthday. The simulation starts on the current date and runs until endOfPlanAge (or ten years out if omitted). endOfPlanAge is exclusive — the plan stops when you reach that age, so endOfPlanAge: 90 simulates through age 89. All dollar amounts in the config are in today’s dollars; the simulation inflation-adjusts them automatically to the date each takes effect (see How amounts are inflated and grown).

The remaining top-level keys — globals, taxes, currencies, symbols, accounts, income, expenses, incomeRules, and actions — are the sections documented below.

Globals

The globals block sets global fallback rates. All fields are optional.

globals:
  currency: $                   # Default currency symbol (denominates all prices)
  inflationRate: 0.025          # Annual inflation for the default currency (2.5%)
  interestRate: 0.04            # Fallback interest rate for currencies (4%)
  equityGrowthRate: 0.07        # Fallback price growth for equity symbols (7%)
  realPropertyGrowthRate: 0.03  # Fallback price growth for real-property symbols (3%)
  couponRate: 0.05              # Fallback yield for bond/treasury symbols (5%)
  dividendRate: 0.02            # Fallback dividend yield for equity symbols (2%)

These rates are the source of truth for anything you don’t override more specifically. A currency without its own entry in currencies uses inflationRate/interestRate; a symbol without its own entry in symbols uses the growth and yield rates for its asset class.

Taxes

The taxes block controls how income is taxed. The simplest form is a single flat effective rate:

taxes:
  effectiveRate: 0.20           # Take a flat 20% of income each year

For more granularity, set per-agency rates. Each of federal, state, city, and fica may be a flat rate (a number), a path to a bracket file, or a jurisdiction code that resolves to a bundled bracket file:

taxes:
  filingStatus: single          # single | married-filing-jointly
  federal: examples/taxes/federal-2024.yaml   # a bracket file
  state: ny                     # a jurisdiction code (looks up the latest bundled file)
  city: nyc
  fica: 0.0765                  # or a flat rate

fica combines Social Security and Medicare and does not model the wage-base cap precisely. See Known limitations for the full list of tax caveats.

Currencies

The currencies map overrides the inflation and interest rates for individual currencies. Each key is an ISO code; both fields are optional and fall back to globals.

currencies:
  USD:
    interestRate: 0.04
  EUR:
    inflationRate: 0.02
    interestRate: 0.03

The default currency is always registered automatically. A currency accrues its own interest and inflation in isolation — there are no cross-currency exchange rates (see Known limitations).

Symbols

The symbols map declares the asset class and rates for the commodity tickers you hold. Each key is a ticker; all value fields are optional and fall back to the class-based defaults.

symbols:
  VOO:
    class: equity               # cash | treasury | equity | bond | real-property
    growthRate: 0.07            # Annual price growth
    dividendRate: 0.02          # Dividend yield (equities)
  TREASURY:
    class: treasury
    couponRate: 0.05            # Coupon yield (bonds/treasuries); alias for the yield
  APT1:
    class: real-property
    price: 400000               # Explicit starting price (else derived from a holding's cost)

dividendRate and couponRate are aliases for the same underlying yield — use whichever reads naturally for the asset. Tickers you hold but don’t list here are resolved from their class-based defaults (an unlisted real ticker is assumed to be equity).

A bond or treasury symbol’s price is held flat — its entire return is the coupon, paid out as income — so a growthRate on one does nothing and is rejected as a config error. Set the yield with couponRate instead. (Price appreciation and a secondary bond market aren’t modeled.)

Synthetic asset-class tickers

You don’t have to name a real security to model an asset class. Four synthetic tickers are built in — one per appreciating/yielding class — so you can hold “an amount of equities” without inventing a symbol:

Ticker Asset class Over time
EQUITY equity price appreciates at the equity rate; pays the dividend yield
BOND bond price held flat; pays the coupon yield as income
TREASURY treasury price held flat; pays the coupon yield as income
PROPERTY real property price appreciates at the real-property rate

Each is a $1-NAV fund — one unit is worth $1 at the plan start — that grows and yields at its class’s global rate, with no symbols: entry required. So a balance of 200000 EQUITY seeds $200,000 of equities, and invest: EQUITY sweeps free cash into equities (see Accounts). Plain currency ($50000) represents cash; there is no synthetic cash ticker.

Because a synthetic ticker is priced at a round $1, it’s ideal when you care about a dollar amount in a class rather than a share count. Use a real ticker (listed in symbols: with its own price, growthRate, and yield) when you want a specific security’s rates or a realistic share price. Listing a symbol whose key is EQUITY/BOND/TREASURY/PROPERTY overrides the built-in defaults for that name.

Accounts

The accounts section lists your starting accounts and seeds them with holdings. Each account maps to an hledger account name and is classified by type, which determines its tax treatment.

accounts:
  - name: assets:bank:checking  # The hledger account name (optional; derived from type if omitted)
    description: Rainy-day cash  # Optional label, distinct from the account name
    type: savings               # savings | brokerage | traditional-401k | roth-ira | hsa | property | loan
    balance: $25000             # Seed holdings (see below)

  - name: assets:brokerage
    type: brokerage
    balances:                   # `balances` (plural) seeds multiple holdings
      - 333 VOO @ $300          # hledger @ syntax: per-share cost at the start of the simulation
      - 333 VTI @@ $99900       # hledger @@ syntax: total cost at the start of the simulation

  - name: assets:hsa
    type: hsa
    balance: $8000

  - name: liabilities:auto-loan # A standalone loan (car, student, credit)
    type: loan
    balance: 18000              # Current balance owed (positive)
    interestRate: 0.06          # Annual rate
    term: 4                     # Remaining years

Seed holdings are given under balance (or balances — synonyms), as one of:

  • a currency amount, e.g. $25000 or 25000 USD
  • a commodity amount, e.g. 100 VOO, optionally with an @/@@ cost annotation
  • a list mixing any of the above
  • the string infer-from-journal to read the balance from the journal (see below)

Account types determine tax treatment:

  • savings, brokerage: taxable accounts
  • traditional-401k: tax-deferred (contributions reduce taxable income, withdrawals taxed as income)
  • roth-ira: tax-free growth (withdrawals not taxed)
  • hsa: tax-free for medical expenses
  • property: real estate, held as a priced commodity. Optional residence: true marks a primary residence, and liquidate: (default true) controls whether the funding cascade may force-sell it to cover a shortfall. A mortgage links to it from the loan side (see loan below), not the other way round.
  • loan: a liability that amortizes rather than growing. Give it a balance: (a number, or infer-from-journal) plus interestRate: and term: (original years), or supply the terms as hforecast-loan-rate:/hforecast-loan-term: journal tags (the tag rate is a percentage, e.g. 6.5). Add a hforecast-loan-start: journal tag with the origination date to shorten the term to what remains today. A level payment posts each period until the balance reaches zero. To finance a property, add propertyAccount: (or a hforecast-property: journal tag) naming the property account; that property’s sale pays the loan off. mortgage is an accepted alias.

If name is omitted, it defaults to assets:<type> (disambiguated with a -2/-3 suffix when several accounts share a type). An account must specify at least a name or a type.

Automatic investing. An account with an invest: allocation converts its free cash into shares each period — so income routed here by incomeRules (or seeded cash) becomes holdings rather than sitting idle. The value is a single ticker (100%) or a ticker→percent map that must sum to 100. The synthetic asset-class tickers (EQUITY, BOND, TREASURY, PROPERTY) need no other setup, but any ticker works.

  - name: assets:brokerage
    type: brokerage
    invest: EQUITY                 # all free cash -> EQUITY
  - name: assets:retirement:401k
    type: traditional-401k
    invest: { EQUITY: 70, BOND: 30 }
    sellOrder: [BOND, EQUITY]      # optional: when liquidated, sell BOND before EQUITY

Percentages split incoming cash; they aren’t rebalanced targets (no selling to hit weights). Without invest:, cash stays cash. For one-off, precise trades use actions: (purchase-stock, …). To change an account’s allocation partway through the plan, use an update-autoinvest action (it retunes what future cash buys; it does not sell what is already held).

Liquidation order. When the funding cascade sells an account’s holdings to cover a shortfall, it drains accounts in the default account-type order (see Funding cascade below), and within an account it sells the optional sellOrder: tickers first (in order), then the safest class first (cash, treasury, bond, equity, real-property).

Inferring from a journal. Instead of literal seed holdings, set balance: infer-from-journal to read the account’s starting holdings from the journal passed with -f (an optional class: forces the asset class of everything held there). See Journal inference.

Income

Income defines earnings. Each entry activates at fromAge and deactivates at toAge; omit either bound to leave that side open.

income:
  - description: Salary          # Optional label
    type: salary                # salary | social-security | general
    period: annual              # one-time (general only) | annual | monthly | biweekly
    amount: 150000              # Amount per period in today's dollars
    growthRate: 0.05            # Annual nominal growth rate (see "Amounts and growth" below)
    fromAge: 30
    toAge: 65
    additionalSalaryOptions:    # Only for salary type
      retirementContribution:   # Diverts part of your paycheck, pre-tax
        account: assets:retirement:401k
        amount: 23000           # Annual pre-tax contribution
      retirementMatch:          # New income from your employer into the account
        account: assets:retirement:401k
        amount: 3000
      hsaSeed:
        account: assets:hsa
        amount: 0
      hsaContribution:
        account: assets:hsa
        amount: 4150

  # An untaxed general income -- a gift, family support, an annuity you aren't taxed on. Recurring
  # here; use period: one-time for a single lump (fires once at fromAge).
  - description: Family support
    type: general
    period: annual
    amount: 12000
    fromAge: 30
    toAge: 40

Salary income automatically calculates federal, state, city, Social Security, and Medicare withholding. Social Security income is not withheld — it is reconciled annually, and is taxable under the usual provisional-income rule.

A general income is an untaxed credit: a gift, family support, or any non-wage income the recipient isn’t taxed on. Like an expense it may be period: one-time (a single credit at fromAge) or recurring, and it lands in income:general, which no tax query reads. Keeping it in income: lets a recurring or scheduled gift sit alongside your other earnings; a one-off tied to an exact date can instead be a receive-income action (see Actions).

Amounts and growth. amount is in today’s dollars. If an income starts in the future, its base is inflated up to the start date so it keeps that purchasing power. From the start date onward a salary or general income grows by growthRate per year — a nominal rate: inflation is not additionally layered on during the income’s life, so growthRate must itself include any cost-of-living increase you want (leaving it at 0 holds the income flat in nominal dollars, eroding its real value). This differs from expenses, which are inflation-adjusted throughout. Social Security is the exception: it tracks inflation as a cost-of-living adjustment for its whole life, so setting a growthRate on a social-security income is rejected as a config error. See How amounts are inflated and grown for the full model across expenses, income, actions, and contributions.

Deriving the amount from a journal. Instead of amount, an entry may take a query (any hledger query, expr: included) whose flow over queryPeriod (last-full-year or year-to-date) is annualized by queryMethod (annualized). Requires -f; mutually exclusive with amount. See Journal inference.

Expenses

Expenses define recurring costs. Like income, they activate by age.

expenses:
  - description: Rent            # Optional label; used in the generated transactions
    type: rent                  # general | rent
    period: monthly             # one-time | annual | monthly | biweekly | daily
    account: expenses:housing:rent
    amount: 1800
    fromAge: 35
    toAge: 90

amount is in today’s dollars and, unlike income, is re-inflated every period, so a recurring expense keeps pace with inflation throughout its life. (Mortgage and loan payments are the exception — they’re fixed in nominal dollars from origination.) See How amounts are inflated and grown.

The rent type is special: it’s automatically skipped once you own property (any account with type: property in state).

An expense may also take a query (with queryPeriod/queryMethod) instead of amount to derive its amount from a journal, exactly as income does above.

The one-time period fires the expense only once, in the year where currentAge == fromAge (toAge does not apply). Keeping a one-off in expenses: lets you sit it right next to any recurring costs it induces — a boat purchase above its slip fees and insurance, say:

  - description: Boat
    period: one-time
    account: expenses:entertainment
    amount: 20000
    fromAge: 45
  - description: Boat upkeep
    period: annual
    account: expenses:boat
    amount: 3000
    fromAge: 45

A one-off can equally be written as a dated spend-money action (see Actions) when you want an exact date rather than an age; both are inflated from today’s dollars.

All expense amounts are inflation-adjusted from the plan start date. If expenses exceed the general account balance, the funding cascade liquidates assets from your configured funding sources to cover the shortfall.

Income rules

Income rules allocate the cash in the general account (where paychecks land, net of taxes) to your other accounts each period, in list order — a “pay yourself first” waterfall. Each rule moves only cash already in general; it never liquidates other accounts. If a target account has an invest: allocation, the cash routed to it is then invested.

incomeRules:
  - description: Emergency fund       # fill savings to $50k as fast as income allows
    strategy: build-target-balance
    targetBalance: 50000
    account: assets:bank:savings
    fromAge: 35
    toAge: 65
  - strategy: contribute-continuously # a fixed annual amount
    account: assets:retirement:roth-ira
    amount: 7000
  - strategy: contribute-remainder    # sweep whatever is left over
    account: assets:brokerage

Every rule is the same capped transfer — min(available, targetBalance − currentValue, amount) — so the strategies differ only in which caps you set:

  • build-target-balance: needs targetBalance (compared against the account’s total value — cash + holdings at market, so it works for invested accounts too, and never overshoots). With an amount it drips that much per period; without one it fills as fast as available cash allows.
  • contribute-continuously: needs amount; contributes it every period regardless of balance.
  • contribute-remainder: no amount or targetBalance; sweeps everything still in general into the account. Place it last to fully drain general each period.

targetBalance and amount are in today’s dollars (inflation-adjusted at runtime). Because rules run in order and each is capped by what’s left in general, earlier rules take priority.

Funding cascade

When the general account can’t cover a cost, hforecast automatically liquidates your other accounts to raise the cash — there is no configuration for this. Accounts are drained in a fixed order by type:

savings → brokerage → hsa → roth-ira → traditional-401k → property

A property account is only force-sold if its liquidate: flag is on (the default). Within an account, holdings sell in the order described under Accounts (sellOrder: first, then safest class first). Withdrawals incur the appropriate treatment — capital-gains tax on taxable holdings, and ordinary income tax plus any early-withdrawal penalty on retirement/HSA accounts, with tax-deferred accounts subject to the bundled retirement regulations. See How it works.

Real estate

Real estate is not a dedicated section — it is a hub-and-spoke of the ordinary building blocks. The property asset account is the hub; a mortgage (type: loan), its costs (expenses:), and its buy/sell events (actions:) all reference it. This gives one source of truth and lets a property carry more than one lien.

Property you already own is a type: property account (inferred from a journal or with literal balances) plus a type: loan account that names it via propertyAccount: (see Accounts and journal inference).

Property you buy during the plan is a buy-property action — its “birth certificate”, since no account exists yet. It creates the type: property account (carrying residence/liquidate), records the purchase, and — when liabilityAccount is given — originates a mortgage and pays closing costs; omit liabilityAccount for a cash purchase. Only account and purchasePrice are required.

actions:
  - action: buy-property
    date: 2033-06-01
    account: assets:real-estate:apartment   # the property asset account this creates
    description: Apartment
    propertyCommodity: APT1                  # optional; defaults to the last account segment, upper-cased
    growthRate: 0.03                   # optional; defaults to the real-property class rate
    purchasePrice: 400000                    # today's dollars
    closingCostsRate: 0.03                   # optional; default 0
    residence: true                          # optional; default false
    liquidate: true                          # optional; default true
    liabilityAccount: liabilities:mortgage:apartment  # omit for a cash purchase
    downPaymentRate: 0.20
    interestRate: 0.065
    loanTerm: 30

Recurring costs are ordinary expenses: tied to the property with propertyAccount:; they are charged only while it is owned (a sale, scheduled or forced, stops them). Use type: property-cost for consumption (HOA, tax, insurance, maintenance) and type: property-improvement for capital improvements — the latter are capitalized into the property’s cost basis, so they shrink the taxed gain at sale rather than being spent outright:

expenses:
  - description: Property tax
    type: property-cost
    period: annual
    account: expenses:taxes:property
    amount: 6000
    propertyAccount: assets:real-estate:apartment
  - description: Improvements
    type: property-improvement
    period: annual
    account: expenses:housing:improvements
    amount: 1000
    propertyAccount: assets:real-estate:apartment

Purchase price and all costs are inflation-adjusted from the plan start date. A sell-property action (below) sells at market value, records the capital gain (basis raised by any capitalized improvements) and closing/broker fees, and pays off every linked lien.

Actions

Actions are one-off imperative events that don’t fit the declarative model — lump-sum transfers, stock trades at specific times, one-time expenses on known dates, or manual rate changes. The summary table below is the quick reference; Actions documents every action’s fields with examples.

In annual mode, actions whose date falls within a given year execute in date order after income and expenses but before growth, so action-driven balance changes are captured by the growth averaging. In daily mode, actions fire on their exact scheduled date.

The transaction is dated at the start of the period the action falls in, not at the action’s own date: daily posts on the exact date, monthly on the 1st of the action’s month, and annual on January 1 of the action’s year. The date’s day (and, in annual mode, month) only picks and orders the action within its period. This is intentional — everything in a period shares its start date so same-period events see one another (for example, a buy-property mortgage must be visible to that period’s loan payment) — so use a finer -g/--granularity when you need actions dated exactly.

actions:
  # transfer-money moves cash between your own accounts; the source must hold the funds.
  # (For a one-off gift or inheritance, use a receive-income action instead.)
  - action: transfer-money
    date: 2030-07-01
    amount: 15000
    account1: assets:bank:checking
    account2: assets:brokerage
    description: Move savings into brokerage

  - action: spend-money
    date: 2035-09-01
    amount: 5000
    account: expenses:travel
    description: Europe trip

  - action: sell-all-stocks
    date: 2060-01-01
    ticker: VOO
    account: assets:brokerage

Available actions:

Action Fields Description
transfer-money amount, account1, account2 Transfer a fixed currency amount between accounts
transfer-all-money account1, account2 Transfer the entire currency balance between accounts
transfer-all-commodities account1, account2 Transfer all commodity holdings between accounts
purchase-stock ticker, shares, account Buy a specific number of shares
purchase-stock-by-amount ticker, amount, account Buy shares worth a currency amount
sell-stock ticker, shares, account Sell a specific number of shares
sell-all-stocks ticker, account Sell the entire position in a commodity
update-autoinvest account, invest Retune an auto-investing account’s allocation mid-plan
spend-money amount, account One-time expense (funded from the general account)
add-expense expenseType, period, amount, account, propertyAccount Add a recurring expense to the live set mid-plan
remove-expense description, account Remove recurring expenses matching a description + account
add-income incomeType, period, amount Add a recurring (non-growing) income mid-plan
remove-income description Remove recurring incomes matching a description
set-income-growth description, growthRate Retune the growth rate of incomes matching a description
buy-property account, purchasePrice, … (see Real estate) Create a property, record the purchase, finance a mortgage
sell-property account, closingCostsRate, brokersFeeRate Sell a property at market value and pay off its linked loans
make-additional-loan-payment account, amount Extra principal against a loan to retire it early (one-off or recurring)
refinance-loan account, interestRate, term, closingCostsRate Refinance a loan: re-amortize its balance at a new rate/term mid-plan
reimburse amount, account1 Credit an amount to an account
set-currency-rate currency, inflationRate, interestRate Change a currency’s inflation and/or interest rate mid-simulation
set-inflation-rate currency, rate Shorthand for set-currency-rate’s inflation-only case
register-symbol ticker, assetClass, growthRate, dividendRate, initialPrice Introduce a new symbol
set-symbol-rate ticker, growthRate, dividendRate, couponRate Retune a known symbol’s growth and/or yield mid-simulation

All actions require date. The repeat, until, and (where applicable) description fields are optional.

sell-property works on any property account — one seeded from a journal (owned real estate) or one created during the plan by a buy-property action. It sells at the current market value, records the capital gain and the closing/broker fees, and pays off every loan linked to the property through its propertyAccount: field / hforecast-property: tag. Because it removes the property from state, firing it also stops that property’s costs and ends any linked loan’s amortization. Each loan also stops on its own once its balance hits zero.