Functional Weave
Code in Python

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

  • overdraft_interest(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 buffer
  • overdraft_interest(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 charged
  • overdraft_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 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.

def overdraft_interest(balances: Sequence[DatedBalance], from_iso: str, to_iso: str, annual_rate_basis_points: int, interest_free_buffer: Money, convention: DayCountConvention, mode: RoundingMode) -> OverdraftInterest
balancesDatedBalance[]the account balance and the date each takes effect, ascending; negative when overdrawn
from_isodatefirst day of the statement period, included
to_isodateend of the period, excluded
annual_rate_basis_pointsintthe overdraft's annual interest rate, 0 or more; 3990 = 39.9%
interest_free_bufferMoneythe overdrawn amount that is free of interest, e.g. the first £250; zero for none
conventionDayCountConventionthe day count basis, usually act-365f
modeRoundingModehow the exact interest becomes whole minor units
returnsOverdraftInterest

The type it declares, generated into your project

@dataclass(frozen=True)
class OverdraftInterest:
    """The charge for the period and how many days drove it."""

    #: interest to charge, zero or more
    interest: Money
    #: days the balance was below zero
    days_overdrawn: int
    #: days the overdrawn amount was above the buffer
    days_charged: int

Your code names it in one line, in the file that uses it

from fune.banking.overdraft_interest import overdraft_interest  # banking.overdraft-interest@^1
impl/python.py · 57 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import List, Sequence

from .banking_overdraft_interest_types import OverdraftInterest
from .dates_day_count_fraction import DayCountConvention  ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
from .dates_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .lending_daily_interest import DatedBalance, daily_interest  ← from lending.daily-interest ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune


def overdraft_interest(
    balances: Sequence[DatedBalance],
    from_iso: str,
    to_iso: str,
    annual_rate_basis_points: int,
    interest_free_buffer: Money,
    convention: DayCountConvention,
    mode: RoundingMode,
) -> OverdraftInterest:
    """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.
    """
    b = annual_rate_basis_points
    if isinstance(b, bool) or not isinstance(b, int) or b < 0:
        raise ValueError("annualRateBasisPoints must not be negative, received %r" % (b,))
    if interest_free_buffer.minor < 0:
        raise ValueError("interestFreeBuffer must not be negative, received %r" % (interest_free_buffer.minor,))
    for entry in balances:
        if entry.balance.currency != interest_free_buffer.currency:
            raise ValueError("currency mismatch: %s and %s" % (entry.balance.currency, interest_free_buffer.currency))
    chargeable: List[DatedBalance] = [
        DatedBalance(
            date=entry.date,
            balance=money(max(0, -entry.balance.minor - interest_free_buffer.minor), entry.balance.currency),
        )
        for entry in balances
    ]
    # Validates the dates, their order and the period before anything is counted.
    accrued = daily_interest(chargeable, from_iso, to_iso, b, convention, mode)
    days_overdrawn = 0
    days_charged = 0
    for i, entry in enumerate(balances):
        start = entry.date if entry.date > from_iso else from_iso
        following = balances[i + 1].date if i + 1 < len(balances) else to_iso
        end = following if following < to_iso else to_iso
        if start >= end:
            continue
        days = days_between(start, end)
        if entry.balance.minor < 0:
            days_overdrawn += days
        if chargeable[i].balance.minor > 0:
            days_charged += days
    return OverdraftInterest(interest=accrued.interest, days_overdrawn=days_overdrawn, days_charged=days_charged)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Python 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
Download for Python banking.overdraft-interest-1.0.0-python.fune · 15,947 bytes sha256 4740d7178eb82eeb063c61caea7498742d5fa0c994987eabf0270da97d5fb4a8

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./banking.overdraft-interest-1.0.0-python.fune, or fetch it from a terminal with fune pull banking.overdraft-interest@1.0.0:python.

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.

CaseArgumentsExpected
£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
CaseArgumentsExpected
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

PathBytes
README.md2,221
impl/python.py2,553
impl/rust.rs3,378
impl/typescript.ts2,552
vectors.json6,868