banking.overdraft-interest
Overdraft interest for a statement period from daily balances, with an interest-free buffer.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates lending figures from published rules. It is a software component for developers, not financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a consumer-credit compliance specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
The interest on a current account's overdraft for one statement period, from its balance history. Each day the account is overdrawn, the amount overdrawn beyond an interest-free buffer accrues interest at the overdraft's annual rate; days in credit, and the part inside the buffer, cost nothing. It also counts the days overdrawn and the days actually charged, which statements show.
## How it is computed
For example
overdraftInterest(balances ×1, 2026-03-01, 2026-04-01, 39.9%, £0.00, act-365f, half-up)→ interest £16.94, days overdrawn 31, days charged 31 £500 overdrawn all month at 39.9%, no bufferoverdraftInterest(balances ×1, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up)→ interest £8.47, days overdrawn 31, days charged 31 a £250 buffer: only the £250 above it is chargedoverdraftInterest(balances ×1, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up)→ interest £0.00, days overdrawn 31, days charged 0 within the buffer all month: no interest
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function overdraftInterest(balances: readonly DatedBalance[], fromIso: string, toIso: string, annualRateBasisPoints: number, interestFreeBuffer: Money, convention: DayCountConvention, mode: RoundingMode): OverdraftInterest
| balances | DatedBalance[] | the account balance and the date each takes effect, ascending; negative when overdrawn |
| fromIso | date | first day of the statement period, included |
| toIso | date | end of the period, excluded |
| annualRateBasisPoints | int | the overdraft's annual interest rate, 0 or more; 3990 = 39.9% |
| interestFreeBuffer | Money | the overdrawn amount that is free of interest, e.g. the first £250; zero for none |
| convention | DayCountConvention | the day count basis, usually act-365f |
| mode | RoundingMode | how the exact interest becomes whole minor units |
| returns | OverdraftInterest |
The type it declares, generated into your project
/** The charge for the period and how many days drove it. */
export interface OverdraftInterest {
/** interest to charge, zero or more */
readonly interest: Money;
/** days the balance was below zero */
readonly daysOverdrawn: number;
/** days the overdrawn amount was above the buffer */
readonly daysCharged: number;
}
Your code names it in one line, in the file that uses it
import { overdraftInterest } from "#fune/banking.overdraft-interest@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type DayCountConvention } from "./dates_day_count_fraction.ts"; ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
import { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { type DatedBalance, dailyInterest } from "./lending_daily_interest.ts"; ← from lending.daily-interest ^1.0.0 · built alongside by fune
import { type OverdraftInterest } from "./banking_overdraft_interest_types.ts";
/**
* Overdraft interest over a statement period. Each day the account is
* overdrawn, the part of the overdrawn amount above the interest-free buffer
* accrues interest; credit balances earn nothing here. The accrual itself is
* lending.daily-interest on those chargeable amounts, so it is summed exactly
* and rounded once for the period.
*/
export function overdraftInterest(
balances: readonly DatedBalance[],
fromIso: string,
toIso: string,
annualRateBasisPoints: number,
interestFreeBuffer: Money,
convention: DayCountConvention,
mode: RoundingMode,
): OverdraftInterest {
if (!Number.isInteger(annualRateBasisPoints) || annualRateBasisPoints < 0) {
throw new RangeError(`annualRateBasisPoints must not be negative, received ${annualRateBasisPoints}`);
}
if (!Number.isInteger(interestFreeBuffer.minor) || interestFreeBuffer.minor < 0) {
throw new RangeError(`interestFreeBuffer must not be negative, received ${interestFreeBuffer.minor}`);
}
for (const entry of balances) {
if (entry.balance.currency !== interestFreeBuffer.currency) {
throw new RangeError(`currency mismatch: ${entry.balance.currency} and ${interestFreeBuffer.currency}`);
}
}
const chargeable: DatedBalance[] = balances.map((entry) => ({
date: entry.date,
balance: money(Math.max(0, -entry.balance.minor - interestFreeBuffer.minor), entry.balance.currency),
}));
// Validates the dates, their order and the period before anything is counted.
const accrued = dailyInterest(chargeable, fromIso, toIso, annualRateBasisPoints, convention, mode);
let daysOverdrawn = 0;
let daysCharged = 0;
for (let i = 0; i < balances.length; i += 1) {
const start = balances[i].date > fromIso ? balances[i].date : fromIso;
const next = i + 1 < balances.length ? balances[i + 1].date : toIso;
const end = next < toIso ? next : toIso;
if (start >= end) continue;
const days = daysBetween(start, end);
if (balances[i].balance.minor < 0) daysOverdrawn += days;
if (chargeable[i].balance.minor > 0) daysCharged += days;
}
return { interest: accrued.interest, daysOverdrawn, daysCharged };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add banking.overdraft-interest
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./banking.overdraft-interest-1.0.0-typescript.fune, or fetch it from a terminal with fune pull banking.overdraft-interest@1.0.0:typescript.
The whole function, every language, is one file too: banking.overdraft-interest-1.0.0.fune, 22,072 bytes, sha256 bf4af41ce4dae6ab3e75bc8132ae144f69be637d26bfcd42deac96d7e9ca6b50. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before banking.overdraft-interest
after — your function gets the result and the arguments, and returns the final result.
// fune: after banking.overdraft-interest
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.day-count-fraction in banking.overdraft-interest
// fune: replace dates.days-between in banking.overdraft-interest
// fune: replace lending.daily-interest in banking.overdraft-interest
// fune: replace math.round-div in banking.overdraft-interest
// fune: replace money.amount in banking.overdraft-interest
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show banking.overdraft-interest --steps.
// fune: step banking.overdraft-interest after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| £500 overdrawn all month at 39.9%, no buffer | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £0.00, act-365f, half-up | → | interest £16.94, days overdrawn 31, days charged 31 |
| a £250 buffer: only the £250 above it is charged | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up | → | interest £8.47, days overdrawn 31, days charged 31 |
| within the buffer all month: no interest | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up | → | interest £0.00, days overdrawn 31, days charged 0 |
| in and out of the overdraft during the month | balances ×4, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up | → | interest £6.29, days overdrawn 15, days charged 15 |
| in credit all month: no interest and no overdrawn days | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £0.00, act-365f, half-up | → | interest £0.00, days overdrawn 0, days charged 0 |
| a balance exactly at the buffer is not charged | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £250.00, act-365f, half-up | → | interest £0.00, days overdrawn 31, days charged 0 |
| opening balance from before the statement | balances ×2, 2026-03-01, 2026-04-01, 19.9%, £0.00, act-365f, half-up | → | interest £7.63, days overdrawn 14, days charged 14 |
| a zero-rate overdraft counts days but charges nothing | balances ×1, 2026-03-01, 2026-04-01, 0%, £0.00, act-365f, half-up | → | interest £0.00, days overdrawn 31, days charged 31 |
| customer-friendly rounding down | balances ×1, 2026-03-01, 2026-03-08, 39.9%, £0.00, act-365f, down | → | interest £0.94, days overdrawn 7, days charged 7 |
| a leap-year February on ACT/365F | balances ×1, 2028-02-01, 2028-03-01, 39.9%, £0.00, act-365f, half-up | → | interest £31.70, days overdrawn 29, days charged 29 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a negative rate is refused | balances ×1, 2026-03-01, 2026-04-01, -0.01%, £0.00, act-365f, half-up | → | error: annualRateBasisPoints must not be negative |
| a negative buffer is refused | balances ×1, 2026-03-01, 2026-04-01, 39.9%, -£0.01, act-365f, half-up | → | error: interestFreeBuffer must not be negative |
| a buffer in another currency is refused | balances ×1, 2026-03-01, 2026-04-01, 39.9%, €0.00, act-365f, half-up | → | error: currency mismatch |
| balances out of order are refused | balances ×2, 2026-03-01, 2026-04-01, 39.9%, £0.00, act-365f, half-up | → | error: strictly ascending date order |
| a first balance after the statement starts is refused | balances ×1, 2026-03-01, 2026-04-01, 39.9%, £0.00, act-365f, half-up | → | error: after fromIso |
More from the author
The balance history is turned into a history of chargeable amounts, max(0, overdrawn − buffer), and handed to lending.daily-interest: each amount accrues for the days it held under the day-count convention, the pieces are summed as exact fractions, and the total is rounded once with the mode you pass. The period is `fromIso` included to `toIso` excluded, and the balance list follows lending.daily-interest's rules (ascending dates, the first on or before `fromIso`).
## The buffer
An interest-free buffer (for example "the first £250 of your arranged overdraft is interest-free") is modelled the common way: only the amount **above** the buffer is charged, every day. A balance exactly at the buffer is not charged. Some accounts instead charge the whole overdrawn amount once it passes a threshold; that is not this rule, and is modelled by passing a zero buffer for the days above the threshold.
## Context
Since April 2020 UK banks price arranged and unarranged overdrafts with a single simple annual interest rate and no fixed daily or monthly fees (FCA, PS19/16 "High-cost credit review: overdrafts", June 2019, https://www.fca.org.uk/publication/policy/ps19-16.pdf), which is exactly what this computes. The rate, buffer, day count and rounding are the account's terms, so they are arguments; ACT/365F and rounding the period's interest to the nearest penny are the usual choices. Credit interest on the same account is lending.daily-interest on the positive balances.
## What it does not do
- Tiered rates (a different rate above some amount): call once per tier with a buffer at the tier's floor and subtract. - Charges other than interest, and monthly caps on charges (see banking.fees-cap). - Rate changes within the period: split the period at the change.
Files
| Path | Bytes |
|---|---|
| README.md | 2,221 |
| impl/python.py | 2,553 |
| impl/rust.rs | 3,378 |
| impl/typescript.ts | 2,552 |
| vectors.json | 6,868 |