BSD-3-Clause licensed and maintained by Peter Simons
This version can be pinned in stack with:hledger-interest-2.0.0@sha256:3ef6643ae59ba7358c03a26a6d0a7cff573ba834c61e619b306bd4b463133789,8150

Module documentation for 2.0.0

There are no documented modules for this package.

hledger-interest

hackage release stackage LTS package stackage Nightly package CI Status

hledger-interest is a small command-line utility based on Simon Michael’s hledger library. Its purpose is to compute interest for a given ledger account. Using command line flags, the program can be configured to use various day counting conventions, such as “act/act”, “30/360”, “30E/360”, and “30E/360isda”. It supports fixed annualized rates and rates that change on specified dates. Rates are expressed as fractions: 0.05 means 5% per year.

Display the available command-line options with hledger-interest --help:

Usage: hledger-interest [OPTION...] ACCOUNT
Default: post at transactions, rate-period ends, and year ends.
  -h          --help                      print this message and exit
  -V          --version                   show version number and exit
  -v          --verbose                   print matching input transactions and generated interest (default)
  -q          --quiet                     print generated interest only
              --today                     alias for --until=today
              --until=DATE                compute interest through DATE (YYYY-MM-DD or today);
                                          exclude transactions with later effective dates
              --post-on-transactions      post only at transactions (and at --until/--today);
                                          skip rate-period and automatic year-end postings;
                                          overrides --post-on-rate-changes
              --post-on-rate-changes      post at transactions and rate-period ends;
                                          skip automatic year-end postings;
                                          --post-on-transactions takes precedence;
                                          --until/--today also post on the final day
  -f FILE     --file=FILE                 input ledger file (pass '-' for stdin)
                                          default: $LEDGER_FILE or ~/.hledger.journal
  -s ACCOUNT  --source=ACCOUNT            interest source account
  -t ACCOUNT  --target=ACCOUNT            interest target account
  -I          --ignore-assertions         ignore any failing balance assertions
              --act                       use 'act/act' day counting convention
                                          (Actual/Actual ISDA; see README)
              --30-360                    use '30/360' day counting convention
              --30E-360                   use '30E/360' day counting convention
              --30E-360isda               use '30E/360isda' day counting convention
              --constant=RATE             fixed annual interest rate
              --annual=RATE               alias for --constant
              --schedule=SCHEDULE         annualized rates and their effective dates;
                                          rates are fractions: 0.05 means 5% per year
                                          syntax: '[(Date1,Rate1),(Date2,Rate2),...]'
              --schedule-file=PATH        read a schedule from a text file;
                                          one date and rate per line, starting in column 1;
                                          rates: 0.05 or 5%; hledger comments allowed
              --annual-schedule=SCHEDULE  alias for --schedule

When run, hledger-interest reads the ledger file designated by the --file flag and selects the postings to the account specified on the command line. The account name must be given in full and is matched literally, including punctuation; selecting a parent account does not include its subaccounts. An account with no matching postings produces an error.

Postings change the balance in effective-date order. The effective date of a posting is the first one present of: the posting’s secondary date, the transaction’s secondary date, the posting’s own date, and the transaction’s date. Interest accrued through that date is posted before the posting changes the balance. Transaction costs (@ and @@) are ignored; interest accrues on the posted amount only.

Generated interest is booked to the account designated by --target, with the opposite amount booked to the account designated by --source. The amounts’ signs depend on the balance and rate. With --verbose, the default, the output also contains every input transaction that touches the selected account, unchanged and including its other postings.

Day counting conventions

For every period, hledger-interest computes

interest = balance * rate * days / days-in-year

where days is the length of the period, counted from the last day already accounted for (for example, the loan date) to the posting date, and days-in-year is the length of the calendar year in which the period ends. Both are counted with the selected convention. For every convention, the day counts of consecutive periods add up to the count over the whole span:

  • --act counts actual calendar days. Calculation splits at calendar-year boundaries, using 365 or 366 days for each year. This is the Actual/Actual ISDA method for every rate option. Internal calculation splits do not cause compounding unless interest is actually posted.
  • --30-360 uses 30-day month offsets and a 360-day year, without adjusting day numbers at month ends.
  • --30E-360 also uses a 360-day year, but replaces the 31st with the 30th when counting days. February retains its actual last day.
  • --30E-360isda additionally treats the last day of February as the 30th. It does not apply the February exception for the maturity date, because hledger-interest has no notion of maturity.

Rate selection and posting frequency are independent. By default, interest is posted on transaction dates, at the end of each rate period, and on December 31st. Posting interest to the interest-bearing account compounds it from the following day.

Posting option Transactions Rate-period ends Year ends
Default Yes Yes Yes
--post-on-rate-changes Yes Yes No
--post-on-transactions Yes No No

--post-on-rate-changes posts at transactions and rate-period ends, without an additional automatic year-end posting. A transaction on December 31st or a new rate taking effect on January 1st still causes a posting on December 31st. --post-on-transactions takes precedence if both flags are supplied, regardless of their order. All modes also post on the final day when --until or --today is used.

With --act, calculations still split internally at year boundaries to apply the correct 365/366-day denominator, even when year-end posting is suppressed. Fixed 360-day conventions need no internal year split unless a year-end posting is requested.

With --post-on-transactions, a posting may combine several rates, so its description omits the rate, for example:

2022-01-10 interest for -9841.10 USD over 31 days
    Liabilities:Loan       -53.65 USD
    Expenses:Interest       53.65 USD

Rounding and precision

Interest is calculated using decimal arithmetic at its available precision. Amounts are not explicitly rounded to cents when they accrue or are posted. If interest is posted to the interest-bearing account, the full computed quantity becomes part of the principal for subsequent calculations. Deferred postings sum the unrounded accrued amounts before displaying them.

Display precision is separate from calculation precision. Generated postings show at least two decimal places, retaining higher display precision from the input. Formatting and display rounding follow hledger-lib’s rendering rules; they do not change the quantity used internally. For example, an interest amount of EUR 0.004 can be displayed as EUR 0.00 while EUR 0.004 is still added to the internal principal when compounding.

Consequently, later displayed balances and interest can differ slightly from calculations made using only the printed amounts. Reading the output journal back in also uses those printed amounts, not the undisplayed fractions. The tool does not implement a policy of rounding each interest posting to the currency’s smallest unit before compounding.

Calculation cutoff

Use --until=YYYY-MM-DD to compute interest through a specific date, including that day. For example, --until=2024-01-10 posts any remaining interest on January 10th, even if there is no transaction on that date. Dates may omit leading zeros in the month and day.

--today is an alias for --until=today, using the current local date. Both options use one cutoff setting: if they are repeated or combined, the last one wins. Without either option, computation stops at the last matching transaction.

Postings with effective dates after the cutoff are ignored, and transactions dated after the cutoff are not echoed. A secondary date takes precedence over the primary date, so a transaction with a later primary date is still included if its secondary date is on or before the cutoff. Postings on the cutoff date are processed normally. A cutoff before the first matching posting produces no output.

Examples

Suppose you borrowed EUR 1000 from your bank at 5% per year and made three repayments. Store the following journal in loan.ledger:

2010-09-26 Loan
    Assets:Bank          EUR 1000.00
    Liabilities:Loan

2010-12-11 Payment
    Assets:Bank          EUR -150.00
    Liabilities:Loan

2011-03-07 Payment
    Assets:Bank          EUR -300.00
    Liabilities:Loan

2011-08-21 Payment
    Assets:Bank          EUR -150.00
    Liabilities:Loan

To post interest to a separate account, run:

$ hledger-interest -f loan.ledger --quiet --act --annual=0.05 -s Expenses:Interest -t Liabilities:Loan:Interest Liabilities:Loan
2010-12-11 5% interest for EUR -1000.00 over 76 days
    Liabilities:Loan:Interest      EUR -10.41
    Expenses:Interest               EUR 10.41

2010-12-31 5% interest for EUR -850.00 over 20 days
    Liabilities:Loan:Interest       EUR -2.33
    Expenses:Interest                EUR 2.33

2011-03-07 5% interest for EUR -850.00 over 66 days
    Liabilities:Loan:Interest       EUR -7.68
    Expenses:Interest                EUR 7.68

2011-08-21 5% interest for EUR -550.00 over 167 days
    Liabilities:Loan:Interest      EUR -12.58
    Expenses:Interest               EUR 12.58

The target account, Liabilities:Loan:Interest, differs from the selected account, Liabilities:Loan, so the posted interest does not itself earn interest. Selecting the parent account does not include its subaccounts. The --quiet flag prints only generated interest postings.

To compound interest, use the loan account itself as the target:

$ hledger-interest -f loan.ledger --quiet --act --annual=0.05 -s Expenses:Interest -t Liabilities:Loan Liabilities:Loan
2010-12-11 5% interest for EUR -1000.00 over 76 days
    Liabilities:Loan       EUR -10.41
    Expenses:Interest       EUR 10.41

2010-12-31 5% interest for EUR -860.41 over 20 days
    Liabilities:Loan        EUR -2.36
    Expenses:Interest        EUR 2.36

2011-03-07 5% interest for EUR -862.77 over 66 days
    Liabilities:Loan        EUR -7.80
    Expenses:Interest        EUR 7.80

2011-08-21 5% interest for EUR -570.57 over 167 days
    Liabilities:Loan       EUR -13.05
    Expenses:Interest       EUR 13.05

Interest posted on December 11th and at year end now becomes part of the principal for subsequent calculations. Add --today to either command to extend the calculation beyond the final repayment through the current date; that additional output depends on when the command runs. Use --until=2011-08-22 instead for a fixed, reproducible cutoff.

Variable annualized rates

Use --schedule to supply annualized rates and their effective dates, together with an explicit day-count convention. For example:

hledger-interest -f loan.ledger --act \
  '--schedule=[(2010-09-26,0.05),(2011-01-01,0.07)]' \
  --source=Expenses:Interest --target=Liabilities:Loan Liabilities:Loan

For the journal above, this applies 5% per year from September 26, 2010, and 7% per year from January 1, 2011. Rates can change on any date. Dates before the first entry earn zero interest, and the last rate continues indefinitely. By default, interest is posted at year ends, at the end of each rate period (the day before the next rate takes effect), and on transaction dates. Use --post-on-rate-changes or --post-on-transactions to control posting as explained above. --annual-schedule remains an alias for --schedule. Likewise, --annual is an alias for --constant; both specify a fixed annualized rate. The executable contains no bank-specific or statutory rate tables; supply the appropriate schedule yourself.

Schedule files

Use --schedule-file=PATH to read the rates from a plain text file instead of specifying a list on the command line. For example, rates.txt could contain:

# Annualized rates and their effective dates
2010-09-26 5%
2011/1/1  0.07  ; equivalent to 7%

comment
This block is ignored.
end comment

Run it with the loan journal above:

hledger-interest -f loan.ledger --act --schedule-file=rates.txt \
  --source=Expenses:Interest --target=Liabilities:Loan Liabilities:Loan

Each entry consists of a hledger date and an annualized rate, separated by spaces or tabs. A fraction such as 0.15 and a percentage such as 15% are equivalent. Signed rates are supported, and whitespace before % or at the end of a line is ignored. Empty and whitespace-only lines are ignored. An entry must begin in column 1: 2026-01-01 0.05 is a syntax error.

Dates accept YYYY-MM-DD, YYYY/MM/DD, or YYYY.MM.DD, with optional leading zeros. As in hledger journals, dates without a year use the current local year. Specify the year explicitly for reproducible schedules.

Comments follow hledger syntax: lines beginning with #, ;, or * (optionally indented) are ignored. A semicolon starts an inline comment; # and * do not. A comment line starts a block that ends at an end comment line or the end of the file. Both block delimiters start in column 1. Windows line endings and a missing final newline are accepted.

File schedules use the same sorting, posting, and rate rules as --schedule. An empty or comments-only file specifies zero interest. When rate options are repeated or combined, the last one wins; overridden files are not read. Read and syntax errors identify the schedule file, with line and column numbers for syntax errors.

Regression tests

Run all tests with:

cabal test all --test-show-details=direct

The calculations suite (tests/calculation-tests.hs) checks the computation engine directly against hand-derived rational values, independently of rendered output. It covers day counts, repayment timing, leap-year denominators, compounding, deferred posting, rate changes, signs, balance transitions, and the rounding policy described above. Comparisons use a tolerance of 10^-30, which accommodates finite decimal representations of recurring fractions while remaining far below any rounding to cents.

The regression suite (tests/golden-tests.hs) runs the executable on the journals in tests/fixtures/ and compares its stdout byte for byte with tests/golden/*.golden. Successful commands must produce no stderr output; error cases check the exit status, empty stdout, and the diagnostic. The cases cover:

  • all four day-count conventions with constant, annual, and scheduled rates, in all three posting modes, given on the command line and in schedule files, with and without --until;
  • leap days, month and year ends, rate changes, same-day payments, compounding versus separate interest accounts, zero and negative rates;
  • secondary and posting-level dates, transaction costs, literal account names, Unicode, and verbose output;
  • multiple currencies, full repayment followed by a new loan, overpayments, and several postings to the selected account;
  • stdin, multiple journals, balance assertions, and invalid arguments;
  • historical rate schedules: BGB288 rates in tests/fixtures/bgb288.rates, DB24 and ING-DiBa rates in tests/HistoricalRates.hs. The DB24 case reproduces the output of the former built-in mode exactly.

Golden cases use explicit years and avoid --today, so their output does not depend on the clock.

To add a case, add its argument list to tests/golden-tests.hs and, if needed, a fixture journal. Missing baselines fail the suite, even with --accept, so create an empty one first (for example, touch tests/golden/new-case.golden). Then record the expected output:

cabal test regression --test-options=--accept
git diff -- tests/golden

Review new and changed golden files before committing them. They capture current behavior, including formatting supplied by hledger-lib, rather than proving the calculations correct, so dependency upgrades may require reviewed baseline updates.

Changes

Revision history for hledger-interest

2.0.0 (2026-11-11)

Incompatible changes

  • The built-in --bgb288, --ing-diba, and --db24 modes have been removed. hledger-interest no longer contains bank-specific or statutory rate tables; supply the rates with --schedule or --schedule-file together with an explicit day-count flag. Unlike the former modes, schedules earn no interest before their first entry and keep the last rate indefinitely. The historical tables survive as regression tests, which also show how to reproduce the former modes’ output.
  • The interest account is matched literally by its full name. Punctuation is no longer interpreted as a regular expression, subaccounts are no longer included, and an account without matching transactions is an error.
  • Rate selection is independent of posting frequency. Every rate option, including --constant, now posts interest at year ends by default; --constant and --annual are equivalent.
  • Interest is no longer computed backwards: balance changes are processed in effective-date order, and overlapping periods are rejected instead of being counted twice.

Changes that affect computed interest

  • --30-360, --30E-360, and --30E-360isda miscounted periods that start on a day adjusted by the convention: a year of monthly payments on the 30th accrued 365, 367, or 358 days instead of 360. Periods are now counted from the last accounted day, so consecutive periods add up to the convention’s count over the whole span. --act is unaffected (#29).
  • Posting-level dates are honored. A posting’s own date, or its secondary date, determines when it changes the balance, taking precedence over the transaction’s dates (#31).
  • Secondary transaction dates are respected when ordering transactions.
  • Transaction costs (@ and @@) no longer leak into the principal, the descriptions, or the generated postings.
  • --act splits calculations at calendar-year boundaries in every posting mode, so each year uses its correct 365/366-day denominator. These internal splits do not compound interest unless it is actually posted.
  • No rate is looked up while the balance is zero, e.g. before the first loan.

New features

  • --until=YYYY-MM-DD computes and posts interest through an inclusive cutoff date and ignores later transactions, adapting Bas van Dijk’s contribution in #19. --today is now an alias for --until=today; the last cutoff option wins.
  • --schedule-file=PATH reads dated annualized rates from a text file. Rates may be given as fractions or percentages, and the file accepts hledger date and comment syntax. Syntax errors report the file, line, and column.
  • --schedule specifies annualized rates and their effective dates; --annual-schedule remains as an alias.
  • --post-on-rate-changes posts interest at transactions and rate-period ends, but not at year ends. --post-on-transactions takes precedence when both flags are given.

Other changes

  • Invalid rate arguments and a missing account argument are reported before the journal is read, and computation errors are shown without exception noise.
  • The --verbose and --quiet help texts describe the output accurately: verbose mode prints the matching input transactions, not the entire journal.
  • The README documents the rounding and precision policy: interest accrues unrounded, and only its display is rounded.
  • A new calculations test suite checks interest amounts and posting dates against hand-derived values, and the golden-file regression suite covers all day-count conventions with all posting modes.
  • Require hledger-lib >= 1.32.

1.7.0 (2026-11-11)

Changes that affect computed interest

Several bugs have been fixed that made hledger-interest compute wrong amounts. Re-running it on existing journals may produce different results in the following cases:

  • --bgb288 applied 5.37% to every date after 2011-06-30, because a stray entry in the table of base rates shadowed all later rates. The table has been fixed, checked against the rates published by the Deutsche Bundesbank, and extended up to 2026-12-31. Dates after the end of the table now fail with an explicit message requesting a rate-table update.
  • --annual-schedule used the rate in effect at the start of a period until the next transaction or year end, ignoring any rate changes in between, and a new rate took effect only on the day after its start date. Rates now take effect exactly on their start date (#20, #23). Dates before the first entry of the schedule earn no interest instead of crashing.
  • A transaction on the first day of a rate period, e.g. on January 1st with --annual, earned one day of interest too many (#21).
  • --30E-360isda counted the days of a period that starts on the 31st of a month wrongly, e.g. 30 instead of 15 days from 2023-01-31 to 2023-02-15 (#28).
  • Interest on balances without decimal places is now shown with at least two decimal places instead of being rounded to whole units.

New features

  • The new --post-on-transactions flag posts interest only on transaction dates (and on the final day with --today). Year and rate boundaries can still split the calculation, but do not themselves trigger posting or compounding (#16, #26).
  • Without --file, hledger-interest now reads $LEDGER_FILE or ~/.hledger.journal like hledger does, instead of silently reading nothing (#24). Errors while reading the journal, such as a missing file, are reported without the “Uncaught exception” noise.

Other changes

  • Descriptions of interest on multi-currency balances no longer contain line breaks (#11, #13).
  • The README explains how interest is computed with each day counting convention, and that --act corresponds to Actual/Actual ISDA (#25).
  • Support hledger-lib up to version 1.52.x and GHC 9.6 to 9.14.
  • Drop the obsolete dependency on Cabal and the Setup.hs file.

1.6.7 and earlier

See the git history.