Interest Accrual
Interest handling has two distinct phases: accrual (calculating how much interest has accumulated) and application (crediting or debiting that interest to the account). These can happen on different schedules and at different precisions.
Ledger movements
Deposits and withdrawals
A savings account is a bank liability -- the bank owes the customer. When a customer deposits cash:
Customer Liability -> Cash
The customer's account is credited (the bank's obligation grows, so a savings balance runs negative in the ledger) and cash is debited: the bank's view, gobank ADR-0008. The goldens are in this direction.
Interest
Interest on a savings account is a cost to the bank. When interest is applied:
-ve Expense:Interest -> Customer Liability
This is a negative movement from the expense account to the customer liability account -- the bank recognises the expense and the customer's balance increases.
For lending products the direction is reversed -- interest is income to the bank and increases the customer's obligation, a positive amount from Income:Interest to the account. An overdrawn current account is lending: drawn past zero it runs positive, like a loan.
Accrual precision
The ledger stores applied balances in minor units (pence for GBP, exponent 2). But during accrual -- while interest is being calculated daily but not yet applied -- the internal representation must use higher precision to avoid accumulated rounding errors.
| Context | Precision | Example (GBP) |
|---|---|---|
| Applied balance (ledger) | Minor units, exponent 2 | 100000 (= GBP 1,000.00) |
| Accrual register (normal) | 5 decimal places | 95890 (= 0.95890 in minor units) |
| Accrual register (high) | Up to 10 decimal places | For products needing sub-basis-point accuracy |
When interest is applied at the end of a period, the accumulated accrual is rounded to minor units and posted as a ledger movement. Any fractional remainder carries forward into the next period.
Exact arithmetic: the 10,000 x 365 denominator
For rates specified to 0.01% (1 basis point) precision, there is an exact representation that avoids all rounding during accrual.
Express the daily interest calculation as an integer fraction:
daily_interest = balance_minor_units * rate_bps / (10000 * 365)
Where rate_bps is the annual rate in basis points (e.g. 350 for 3.50%). The denominator 10000 * 365 = 3,650,000 is fixed.
Example: GBP 10,000.00 at 3.50%:
daily_interest = 1000000 * 350 / 3650000
= 350000000 / 3650000
= 95.8904109589... minor units
The numerator (350,000,000) and denominator (3,650,000) are both integers. During accrual, accumulate the numerator and defer the division. At application time, divide, post the integer part to the ledger, and carry the remainder numerator forward.
This is the no-rounding option: interest specified to 0.01% annual gross is exactly representable, and no precision is lost during accrual. The only rounding occurs at application, where the fractional minor unit is carried forward rather than lost.
For actual/actual day count (see below), the denominator becomes 10000 * days_in_year (3,650,000 or 3,660,000).
Day count conventions
The day count convention determines how the annual rate is converted to a daily rate.
| Convention | Daily divisor | When to use |
|---|---|---|
| Actual/365 (preferred) | Always 365 | UK money markets, most UK savings and lending products |
| Actual/Actual | 365 or 366 depending on leap year | Some bond markets, EU regulatory contexts |
Actual/365 is the recommended default. It is simpler (fixed denominator) and standard for UK retail banking. In a leap year, 366 days of interest are charged against a 365 divisor, so the effective annual rate is slightly higher than the nominal rate.
Actual/Actual uses the actual number of days in the year as the divisor. In a leap year the daily rate is lower (divided by 366), so 366 days at the lower rate gives exactly the nominal annual rate. This is more "correct" but less common in UK retail products.
The day count convention is configured per product alongside the application period.
Accrual and application periods
Products choose independent schedules for accrual and application:
| Period | Accrual | Application |
|---|---|---|
| Daily | Calculate interest each day | Post to ledger each day |
| Monthly | Calculate interest each day, accumulate | Post to ledger on month-end |
| Quarterly | Calculate interest each day, accumulate | Post to ledger on quarter-end |
| Annually | Calculate interest each day, accumulate | Post to ledger on year-end |
Accrual is always daily -- the balance can change any day, so interest must be tracked daily regardless of when it is applied. The application period controls when accumulated interest is posted to the account as a ledger movement.
Recommendation
Daily accrual, monthly application is the recommended default for most products. It matches UK high-street savings account conventions and provides a good balance between accuracy and ledger volume.
Daily application (the current default) is appropriate for products where daily compounding is an explicit feature, or for simplicity during early development.
| Application period | When to use |
|---|---|
| Daily | Simple products, early development, products advertising daily compounding |
| Monthly | Recommended. Standard UK savings and lending convention |
| Quarterly | Some NS&I products, specific fixed-term bonds |
| Annually | Long-term fixed-rate bonds, some ISA products |
How application period affects compounding
When interest is applied, it becomes part of the balance and itself earns interest. More frequent application means more compounding, which increases the effective annual return.
Example: 5.00% nominal rate on GBP 10,000 for one year (actual/365):
| Application period | Compounding events/year | Year-end balance | Effective annual rate |
|---|---|---|---|
| Daily | 365 | GBP 10,512.67 | 5.1267% |
| Monthly | 12 | GBP 10,511.62 | 5.1162% |
| Quarterly | 4 | GBP 10,509.45 | 5.0945% |
| Annually | 1 | GBP 10,500.00 | 5.0000% |
The difference is small but meaningful for regulatory disclosure and product comparison.
AER (Annual Equivalent Rate)
AER is the annualised effective rate that accounts for compounding. It allows customers to compare products with different application periods on a like-for-like basis.
AER = (1 + r/n)^n - 1
Where r is the nominal annual rate and n is the number of application periods per year.
| Application period | n | Formula |
|---|---|---|
| Daily | 365 | (1 + r/365)^365 - 1 |
| Monthly | 12 | (1 + r/12)^12 - 1 |
| Quarterly | 4 | (1 + r/4)^4 - 1 |
| Annually | 1 | r (no compounding effect) |
AER is a savings product concept. UK regulations (BCOBS) require savings products to display AER so customers can compare rates. The AER is derived from the nominal rate and the application period -- changing the application period changes the AER even if the nominal rate stays the same.
The product specification sets the nominal annual_rate. The AER is computed from this rate and the configured application period. This conversion will live in gobank-products so that product documentation and customer-facing rates are consistent.
APR (Annual Percentage Rate)
APR is the lending-side equivalent of AER. It represents the total annual cost of borrowing, including compounding and (eventually) fees.
For a simple interest-only calculation:
APR = (1 + r/n)^n - 1
This is the same formula as AER when only interest is considered. The distinction matters when fees are included -- APR folds arrangement fees, annual fees, and other charges into the effective rate.
APR applies to: Personal Loan, Mortgage, Overdraft.
Status: future feature. The current implementation does not include fee modelling, so APR and AER are numerically identical. APR will diverge once fee features are added.
Product configuration
Each product specifies its interest method via parameters:
| Parameter | Values | Default |
|---|---|---|
annual_rate |
Decimal (e.g. 0.035) |
Set per product |
interest_application |
daily, monthly, annual |
monthly (quarterly not yet) |
day_count |
actual_365, actual_actual |
actual_365 |
accrual_precision |
5, 10, or exact |
5 |
When accrual_precision is exact, the 10,000 x 365 integer fraction method is used with no rounding until application.
Product recommendations
| Product | Application | Day count | Rationale |
|---|---|---|---|
| Easy Access | Monthly | Actual/365 | Standard UK savings convention |
| Fixed Term | Monthly or Quarterly | Actual/365 | Matches typical bond terms |
| ISA | Monthly | Actual/365 | Standard UK ISA convention |
| Personal Loan | Monthly | Actual/365 | Aligns with repayment schedule |
| Mortgage | Monthly | Actual/365 | Aligns with repayment schedule |
| Overdraft | Daily | Actual/365 | Interest charged daily on fluctuating balances |
Implementation notes
Implemented in gobank-products v0.1.10 (the calculation lives in the product layer, not go-luca):
- Accrual register --
ManagedAccount.AccruedNumeratorholds accrued-but-unapplied interest as an exact integer fraction overAccrualDenominator(10,000 x 365). Daily accrual addscached_balance_minor_units * rate_bps; no rounding occurs until application, and the sub-minor-unit remainder carries forward, so small balances never lose interest. - Cached balances --
ManagedAccount.CachedBalanceis maintained bySimulation.RecordMovement, so daily accrual issues no balance queries.Simulation.RefreshBalancesre-primes the cache after a ledger import. - The day rule,
Product.NextDay-- a pure function from an account's position at the end of the previous day (go-lucaPosition: balance and accrued interest as an exact fraction) and the day's closing balance to the position at the end of this day and the ledger postings it calls for. Interest accrues on the closing balance; when the product'sinterest_applicationcycle ends on this day the whole minor units are applied and the remainder carries forward. Application is therefore product code inside the daily pass, not a separate month-end run. It isApplyofAccrue:Product.Accruegives the day's position with the interest accrued and nothing applied, the same for any closing balance, so a caller projecting a day before it is over writes it provisionally and rewrites it as events move the balance;Product.Applybooks the cycle-end application once the closing balance is final, and applying an applied position posts nothing.InterestAccrualruns it atEndOfDay(idempotent per date viaLastAccrued, so accounts can be done individually, spread across the day) and posts what it returns. - Ledger directions -- savings apply as
Expense:Interest -> account; lending asIncome:Interest -> account(increases the customer obligation).
Still open: a quarterly cycle, day_count conventions beyond actual/365, closed-form multi-day accrual between change events (a month at a time), AER/APR computation, and rate changes mid-period.