Functional Weave
Code in Rust

lending.apr@1.0.1

README.md

4,825 bytes · view raw

# lending.apr

**Status: needs review by a consumer-credit specialist before publishing.**

The annual percentage rate of charge for a regulated consumer credit
agreement: the single rate that makes what the lender advances worth the same
as everything the borrower pays, as defined by the total charge for credit
rules.

## The rule

FCA Handbook, CONC App 1.2.6R, "Total charge for credit rules for other
agreements" (App 1.1 covers certain agreements secured on land). These rules
took over from the Consumer Credit (Total Charge for Credit) Regulations 2010
when consumer credit moved to the FCA in 2014:

    Σ C_k (1 + X)^(−t_k)  =  Σ D_l (1 + X)^(−s_l)

drawdowns C_k at t_k years, repayments and charges D_l at s_l years, both
measured from the first drawdown; X is the APR. App 1.2.6(3): a year is
365 days (366 in a leap year), 52 weeks or 12 equal months; and "the result
of the calculation shall be expressed with an accuracy of at least one
decimal place; if the figure at the following decimal place is greater than
or equal to 5, the figure at that particular decimal place shall be increased
by one." Source: https://www.handbook.fca.org.uk/handbook/CONC/App/1/2.html,
read 2026-09-23.

## Inputs

Each flow is a whole number of periods after the first drawdown, and
`periodsPerYear` says what a period is: 12 (months), 52 (weeks), 365 (days),
4, 2 or 1. So a monthly loan is described in months and t = months / 12, and
a 30-day loan in days with t = days / 365. The earliest advance must be at
period 0. Fees and charges the borrower pays are repayments, at the period
they are paid (an arrangement fee at drawdown is a repayment at period 0).
Every amount must be positive and in one currency.

## How it is solved

With v = (1 + X)^(−1/periodsPerYear), the equation becomes a polynomial in v
with whole exponents: Σ (net flow at t) × v^t = 0. It is solved by bisection
on v between 0 and 1 in 18-place fixed point (math.fractional-power's
`powFixed`, the same floors in the same order in every language), about 60
halvings until v is pinned to 10^-18. Then X = v^(−periodsPerYear) − 1.

Precision: the unrounded APR is accurate to around 10^-14. It is first
settled to ten decimal places of the rate (10^-8 of a percentage point),
which removes the solver's last-digit noise so an APR of exactly 12.65% is
12.65 and not 12.6499999, and then rounded as App 1.2.6(3)(f) says: to one
decimal place, rounding up when the second decimal place is 5 or more. That
is half-up, so 12.65% is 12.7% and 12.64% is 12.6%. `preciseBasisPoints`
gives the settled APR to two decimal places (half-up) for checking.

The root is unique when the flows change sign once: all drawdowns (net) come
before all repayments (net). Flows that interleave, such as a second drawdown
after a repayment, can have more than one solution and are refused rather
than answered with whichever root bisection finds. Repayments that total less
than the credit (a negative APR) are refused; exactly equal totals are 0.0%.

## What a specialist should check

- The leap-year rule: "366 days for leap years" is not applied. Day-based
  flows use t = days / 365 throughout, the common industry reading; a
  specialist should confirm that for agreements spanning 29 February.
- Which charges go into the total charge for credit, and the assumptions of
  CONC App 1.2 (for example for running-account credit and the assumed
  drawdown and repayment patterns of App 1.2.7 onwards) are the caller's.
  This computes the rate from the flows it is given.
- Whether whole periods are fine enough: a first payment 45 days after
  drawdown on a monthly loan must be described in days, with every flow in
  days.

## Before you rely on this

**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 above, 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.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified consumer-credit compliance specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

1.0.1 marks it unreviewed. The code and the tests are unchanged.