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:30is two tags but; hforecast-loan-rate:6.5 ; hforecast-loan-term:30folds 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 baretype:/class:. hledger reserves the accounttype:tag for its own account-type taxonomy (Asset/Liability/Cash/…) and will refuse to parse a journal that uses an unrecognized value, soaccount … ; type:brokeragewould make your journal unloadable everywhere. Thehforecast-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 asqty 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-journalrequires-f, and it’s one value of thebalancekey, 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.typeandclasshere 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
queryis any hledger query, includingexpr:boolean queries.- A
queryrequires-fand is mutually exclusive withamount(set exactly one). - Defaults for a queried entry: income becomes
type: salary(so it flows through withholding), both default toperiod: 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
assets:brokerage 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):
- an explicit
symbols:entry for the ticker, - a journal commodity tag
hforecast-asset-class:, - a per-account
class:field in YAML, - a journal account tag
hforecast-asset-class:(inherited), - 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-commodityhforecast-asset-class:tag overrides it. Split a mixed account into subaccounts, or pin the odd ticker undersymbols:. - 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
queryPeriodpresets exist; raw hledger period expressions and additionalqueryMethods are future additions. - See Known limitations for how inferred cost basis and valuation behave.