Skip to content

Documentation

Journal tagging and inference

If you keep an hledger journal, pass it with -f and let hforecast pull numbers straight from it: an account’s starting holdings from your live balances, and recurring income/expense amounts from your history. You still describe the shape of the plan (which accounts, ages, bounds) in YAML — inference fills in the numbers. It is opt-in per field, never automatic: nothing is inferred unless you ask for it.

This document covers the journal tags hforecast understands and how to turn on inference in your hforecast.yaml. For a runnable end to end example, see examples/journal-user.yaml and examples/journal-user.journal:

hforecast -c examples/journal-user.yaml -f examples/journal-user.journal --explain >/dev/null

Add --explain (as above) to any run to see exactly what was inferred, priced, and classified, each value tagged with where it came from.

Journal tags

hforecast reads a handful of optional tags from your journal. They let the journal describe itself so your YAML can stay minimal. Requires hledger ≥ 1.52 (older versions discard commodity-directive tags).

Tag Put it on Values Purpose
hforecast-account-type: an account directive savings, brokerage, traditional-401k, roth-ira, hsa, property, loan (alias mortgage) the account’s tax treatment / role
hforecast-asset-class: a commodity directive cash, treasury, equity, bond, real-property a commodity’s asset class (how it grows)
hforecast-asset-class: an account directive (same as above) a class fallback for every commodity held in that account
hforecast-loan-rate: a loan account directive a percentage, e.g. 6.5 the loan’s annual interest rate
hforecast-loan-term: a loan account directive years, e.g. 30 (fractional allowed) the loan’s original term
hforecast-loan-start: a loan account directive a date, YYYY-MM-DD when the loan was originated (shortens the term)
hforecast-property: a loan account directive a property account name links a loan to the property it finances (paid off on that property’s sale)
; the account's tax type
account   assets:retirement:401k   ; hforecast-account-type:traditional-401k

; a commodity's asset class (grows at the class's rate)
commodity VBTLX                    ; hforecast-asset-class:bond
commodity VNQ                      ; hforecast-asset-class:real-property

; a class fallback for a whole account's holdings
account   assets:brokerage:bonds   ; hforecast-asset-class:bond

; a mortgage with its forward terms (comma-separated tags on one line); the 6.5% rate is a
; percentage, and the 30-year term is shortened by the origination date to what remains today.
; hforecast-property: links it to the home it finances, so the home's sale clears it.
account   liabilities:mortgage:home ; hforecast-account-type:loan, hforecast-property:assets:real-estate:home, hforecast-loan-rate:6.5, hforecast-loan-term:30, hforecast-loan-start:2020-01-01

; the property it secures
account   assets:real-estate:home   ; hforecast-account-type:property

Separate multiple tags on one directive with commas, not semicolons. hledger reads a tag’s value up to the next comma or end of line, so ; hforecast-loan-rate:6.5, hforecast-loan-term:30 is two tags but ; hforecast-loan-rate:6.5 ; hforecast-loan-term:30 folds everything after the first colon into a single value.

Account tags are inherited: tagging assets:brokerage applies to assets:brokerage:* too.

Use the namespaced hforecast- tags, not bare type: / class:. hledger reserves the account type: tag for its own account-type taxonomy (Asset/Liability/Cash/…) and will refuse to parse a journal that uses an unrecognized value, so account … ; type:brokerage would make your journal unloadable everywhere. The hforecast- prefix keeps our tags collision-free.

Turning on inference in YAML

Accounts — seed balances from the journal

Set balance: infer-from-journal on an account. Its starting holdings are read from the journal balance of that account name as of the plan start. The name is matched as an hledger account query, so assets:bank covers every assets:bank:* subaccount (their balances aggregate into the one seeded node).

accounts:
  - name: assets:bank                 # aggregates assets:bank:*
    type: savings
    balance: infer-from-journal
  - name: assets:brokerage
    balance: infer-from-journal       # type + per-commodity class come from journal tags
    # class: equity                   # optional: force one class for everything held here
  • Balances come straight from the journal, so they always reflect your latest data. Once inference has read them, each inferred balance is attached to its account just like a literal balance: list (cash, plus each holding as qty COMMODITY @ current-price), and hforecast mints them into one opening-balances transaction at the plan start. The fed-in history itself is dropped — the simulation runs forward from that opening snapshot rather than re-scanning your whole ledger every period (a large journal would otherwise slow every step).
  • infer-from-journal requires -f, and it’s one value of the balance key, so a literal balance and inference can’t be mixed on one account. A journal you pass without inferring from it is not carried into the output; only accounts you declare (literal or inferred) seed the opening balances.
  • type and class here are optional overrides; when omitted they come from journal tags or defaults (see precedence below).

Loans and owned real estate

A type: loan account is a liability: it carries a negative balance and amortizes rather than growing. Its balance is either literal (balance: 18000) or inferred (balance: infer-from-journal, read as the magnitude of the journal’s liability node); its forward terms come from YAML interestRate:/term: or the hforecast-loan-rate:/hforecast-loan-term: journal tags. Each period a level payment is split into interest and principal, and payments stop automatically once the balance reaches zero. Add a hforecast-loan-start: tag with the origination date and the tool shortens the original term to what remains today, so a year-old 30-year loan amortizes over ~29 years rather than a fresh 30.

An already-owned home is just a property account (an inferred, priced real-property commodity) plus a separate loan account for the mortgage. The link lives on the loan — its propertyAccount: field (or a hforecast-property: tag) names the property it finances — so a sale can pay the mortgage off:

accounts:
  - name: assets:real-estate:home     # priced by a `P` directive in the journal
    type: property
    balance: infer-from-journal
    residence: true                   # optional; primary-residence marker (default false)
  - name: liabilities:mortgage:home
    type: loan
    balance: infer-from-journal       # balance from the journal
    interestRate: 0.065               # a fraction; or a hforecast-loan-rate:6.5 tag (a percentage)
    term: 30                          # original years; or a hforecast-loan-term: tag
                                      # (add a hforecast-loan-start: tag to count elapsed time)
    propertyAccount: assets:real-estate:home  # or a hforecast-property: tag on this loan

To model real estate purchased during the plan, use a buy-property action instead — it creates the property account and links its mortgage at the purchase date. This account-based form is for property you already own at the start of the plan. A sell-property action disposes of either kind and clears every linked loan (see Configuration).

Income and expenses — derive amounts from history

Give an income or expense a query instead of an amount. The matched flow over a historical window is annualized into a recurring amount.

income:
  - description: Salary
    query: income:salary
    # queryPeriod: last-full-year     # default | year-to-date
    # queryMethod: annualized         # default (only method for now)
    toAge: 65

expenses:
  - description: Discretionary
    query: "expr:expenses:discretionary AND not:desc:Rent"
    toAge: 65
  - description: Insurance             # literal amounts still work alongside queries
    amount: 2000
    period: annual
    toAge: 65
  • query is any hledger query, including expr: boolean queries.
  • A query requires -f and is mutually exclusive with amount (set exactly one).
  • Defaults for a queried entry: income becomes type: salary (so it flows through withholding), both default to period: annual, and the start age anchors to your current age (so a queried salary’s growth doesn’t back-compound from age 0).

How it resolves

Balances and prices

At the plan start, each inferred account’s holdings are its journal balance as of that date. The default-currency portion is cash; every other commodity is a share position that must be priced in the default currency so it can be valued and grown. Prices come from hledger’s inferred market prices — a P directive or an @/@@ cost on a transaction:

P 2025-12-31 VOO $450         ; an explicit market price
2020-01-01 Buy
      100 VOO @ $300   ; a cost hledger can infer a price from

A held commodity with no price path is a hard error (rather than a silent guess). Fix it by adding a P directive, an @/@@ cost, or a price: under symbols:.

Asset class of each commodity

The class decides the growth and yield a holding gets. It is resolved in this order (first match wins):

  1. an explicit symbols: entry for the ticker,
  2. a journal commodity tag hforecast-asset-class:,
  3. a per-account class: field in YAML,
  4. a journal account tag hforecast-asset-class: (inherited),
  5. otherwise equity — except in a property-type account, where holdings default to real-property. This last step is a guess; hforecast collects every such commodity into a single warning suggesting you tag them.

A commodity that was bought and later fully sold nets to zero and is ignored (it does not appear as a holding).

Account tax type

Resolved as: an explicit YAML type → a journal hforecast-account-type: account tag (inherited) → savings. Inferred accounts that fall back to savings (no type and no tag) but hold a non-zero balance are collected into a single warning — worth checking, especially for tax-advantaged accounts like a 401k, Roth, or HSA whose treatment depends on the right type.

Income and expense amounts

queryPeriod selects the window the query sums over:

  • last-full-year (default) — the most recent complete calendar year (e.g. all of 2025 for a 2026 run).
  • year-to-date — January 1 of the current year through the plan start.

queryMethod turns that flow into a recurring amount:

  • annualized (default and only method) — the matched total is scaled to a full year (total × 365 ÷ days-in-window). A full-year window returns that year’s total; a partial window returns an annualized run-rate. Income posts as a credit, so it is negated to a positive amount automatically; expenses (debits) are used as-is.

--explain prints the exact calculation, e.g. Σ = $-150,000 → negate → ×365d → $150,000/yr.

Growth and yield of inferred holdings

A holding grows according to its class, using the rates in your globals: block (or the built-in defaults): equity/real-property appreciate at their growth rate and equities pay the dividend yield; bonds/treasuries pay the coupon rate; cash earns interest. Override a specific ticker under symbols: when you want rates that differ from its class.

Reference

Account types (hforecast-account-type: tag or YAML type): savings, brokerage, traditional-401k, roth-ira, hsa, property, loan (alias mortgage).

Asset classes (hforecast-asset-class: tag or YAML class): cash, treasury, equity, bond, real-property.

Query periods: last-full-year, year-to-date. Query methods: annualized.

Limitations

  • Classification is per account: every commodity in an account shares a class unless a symbols: entry or a per-commodity hforecast-asset-class: tag overrides it. Split a mixed account into subaccounts, or pin the odd ticker under symbols:.
  • An unpriceable holding is an error, not a guess.
  • Collapsing many tickers into synthetic per-class units (simplifySymbols) is not yet supported; it is parse-rejected so it isn’t silently ignored.
  • Only the two named queryPeriod presets exist; raw hledger period expressions and additional queryMethods are future additions.
  • See Known limitations for how inferred cost basis and valuation behave.