hledger-interest: computes interest for a given account

[ bsd3, finance, program ] [ Propose Tags ] [ Report a vulnerability ]

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 schemes for day-counting, such as act/act, 30/360, 30E/360, and 30E/360isda. It supports constant rates, annual interest with a fixed rate, and schedules of annual rates supplied on the command line or read from a text file.

As an example, consider the loan in the regression fixture testsfixturesboundaries.journal:

2023-01-31 Loan
    Assets:Bank          EUR 10000.00
    Liabilities:Loan

2023-02-28 Payment
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2023-03-31 Payment
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2023-07-01 Payment on rate change
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2023-07-01 Second payment on same day
    Assets:Bank          EUR -50.00
    Liabilities:Loan

2024-01-15 Payment after year end
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2024-02-29 Leap day payment
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2024-03-31 Payment
    Assets:Bank          EUR -100.00
    Liabilities:Loan

2024-04-01 Unrelated transaction
    Assets:Bank          EUR -10.00
    Expenses:Other

With 5% annual interest and actual-day counting, crediting interest to the loan account compounds it. The following command prints only the generated interest postings; its output is checked by the annual-act golden test:

$ hledger-interest --file=tests/fixtures/boundaries.journal --source=Expenses:Interest --target=Liabilities:Loan --act --annual=0.05 --quiet Liabilities:Loan
2023-02-28 5% interest for EUR -10000.00 over 28 days
    Liabilities:Loan       EUR -38.36
    Expenses:Interest       EUR 38.36

2023-03-31 5% interest for EUR -9938.36 over 31 days
    Liabilities:Loan       EUR -42.20
    Expenses:Interest       EUR 42.20

2023-07-01 5% interest for EUR -9880.56 over 92 days
    Liabilities:Loan      EUR -124.52
    Expenses:Interest      EUR 124.52

2023-12-31 5% interest for EUR -9855.08 over 183 days
    Liabilities:Loan      EUR -247.05
    Expenses:Interest      EUR 247.05

2024-01-15 5% interest for EUR -10102.13 over 15 days
    Liabilities:Loan       EUR -20.70
    Expenses:Interest       EUR 20.70

2024-02-29 5% interest for EUR -10022.84 over 45 days
    Liabilities:Loan       EUR -61.62
    Expenses:Interest       EUR 61.62

2024-03-31 5% interest for EUR -9984.45 over 31 days
    Liabilities:Loan       EUR -42.28
    Expenses:Interest       EUR 42.28

Running the utility with --help gives a brief overview over the available options:

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

[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 1.0, 1.1, 1.2, 1.3, 1.4, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.5, 1.5.1, 1.5.2, 1.5.3, 1.5.4, 1.5.5, 1.6.0, 1.6.1, 1.6.2, 1.6.3, 1.6.4, 1.6.5, 1.6.6, 1.6.7, 1.7.0, 2.0.0
Change log CHANGELOG.md
Dependencies base (>=4.9 && <5), Decimal, hledger-lib (>=1.32 && <1.53), megaparsec (>=7), mtl, text, time [details]
Tested with ghc ==9.6.7, ghc ==9.8.4, ghc ==9.10.3, ghc ==9.12.2, ghc ==9.14.1
License BSD-3-Clause
Author Peter Simons <simons@cryp.to>
Maintainer Peter Simons <simons@cryp.to>
Uploaded by PeterSimons at 2026-11-11T17:42:21Z
Category Finance
Home page https://github.com/peti/hledger-interest
Source repo head: git clone https://github.com/peti/hledger-interest.git
Distributions LTSHaskell:1.6.7, Stackage:2.0.0
Reverse Dependencies 1 direct, 0 indirect [details]
Executables hledger-interest
Downloads 14802 total (110 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs not available [build log]
Last success reported on 2026-11-11 [all 1 reports]

Readme for hledger-interest-2.0.0

[back to package description]

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.