finanza/interest

Time-value-of-money helpers built on finanza/decimal.

Every function takes its inputs as decimals, computes in decimal, and rounds the final result with HalfEven (“banker’s”) to the caller-supplied number of decimal places.

Precision

Iterative computations (future_value, present_value, payment, effective_annual_rate, compound_interest) cap their internal working precision at 6 decimal digits. The cap exists so that two N-digit coefficients multiplied together stay under 2^53 - 1 (the JavaScript safe-integer ceiling enforced by finanza/decimal): at 6 digits, (10^6) × (10^6) = 10^12, comfortably below 2^53 ≈ 9 × 10^15.

Concrete consequence: for inputs with exact rates at ≤ 6 dp over short horizons (≤ ~36 periods), results match textbook full-precision values to the cent. For inputs with inexact rates (e.g. 0.04 / 12 = 0.003333… truncated to 6 dp) or long horizons (180+ periods, daily compounding over years), the per-iteration rounding accumulates and the final value can differ from a textbook 50-digit reference (Python decimal, numpy_financial, Excel) by 0.01–0.10 in digits = 2 outputs, or a few parts-per-million in digits = 6 outputs.

Examples of drift versus textbook references:

ScenarioTextbookfinanzaDrift
PMT 200_000 @ 4%/12 monthly, n=180, dg=21479.381479.340.04
PMT 50_000 @ 3.5%/12 monthly, n=84, dg=2671.99672.010.02
EAR 5% nominal / daily (365), dg=60.0512670.0512725 ppm
FV 1000 @ 5% for 10 periods, dg=61628.8946271628.8940000.0006

For “kitchen-table” and educational use, the drift is invisible. For regulated lending, mortgage amortisation tables, or any flow whose payments must match what an external regulator recomputes, do not rely on these functions; reach for a higher-precision library or compute the closed form externally. Re-raising the cap to recover textbook accuracy requires reworking the precision-overflow guard so that intermediate products stay under 2^53 - 1 — tracked in #9.

Types

Errors raised by interest functions.

pub type InterestError {
  NegativePrincipal
  NegativeRate
  PeriodsOutOfRange
  CompoundsOutOfRange
  NegativeDigits
  ArithmeticError(error: decimal.ArithmeticError)
}

Constructors

  • NegativePrincipal

    principal was negative.

  • NegativeRate

    rate was negative.

  • PeriodsOutOfRange

    periods was zero or negative, or exceeded the supported range 1..=1200 (100 years of monthly compounding).

  • CompoundsOutOfRange

    compounds_per_year was zero or negative.

  • NegativeDigits

    digits was negative.

  • ArithmeticError(error: decimal.ArithmeticError)

    Underlying decimal arithmetic produced an error.

Values

pub fn compound_interest(
  principal principal: decimal.Decimal,
  annual_rate annual_rate: decimal.Decimal,
  years years: Int,
  compounds_per_year compounds_per_year: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Future value under compound interest:

FV = principal × (1 + annual_rate / compounds_per_year)^(compounds_per_year × years)
pub fn effective_annual_rate(
  nominal_rate nominal_rate: decimal.Decimal,
  compounds_per_year compounds_per_year: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Effective annual rate from a nominal rate compounded compounds_per_year times per year:

EAR = (1 + nominal_rate / compounds_per_year)^compounds_per_year - 1

See the module-level Precision section for the 6-working-digit cap and the resulting drift vs textbook references at high compounding frequencies (e.g. daily / 365).

pub fn future_value(
  present present: decimal.Decimal,
  rate_per_period rate_per_period: decimal.Decimal,
  periods periods: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Future value of present after periods periods at rate_per_period.

See the module-level Precision section for the 6-working-digit cap and the resulting drift vs textbook references on long horizons or with inexact rates.

pub fn payment(
  principal principal: decimal.Decimal,
  rate_per_period rate_per_period: decimal.Decimal,
  periods periods: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Periodic payment for a fully-amortising loan:

PMT = principal × rate / (1 - (1 + rate)^(-periods))

When rate_per_period is zero, returns straight-line principal / periods.

See the module-level Precision section for the 6-working-digit cap. For long amortising loans (e.g. a 15-year monthly mortgage = 180 periods) the cent value of the returned payment can drift from a textbook PMT computed in 50-digit precision by up to ~0.04 — too much for regulated lending.

pub fn present_value(
  future future: decimal.Decimal,
  rate_per_period rate_per_period: decimal.Decimal,
  periods periods: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Present value of future discounted at rate_per_period for periods periods.

See the module-level Precision section for the 6-working-digit cap and the resulting drift vs textbook references on long horizons or with inexact rates.

pub fn simple_interest(
  principal principal: decimal.Decimal,
  rate rate: decimal.Decimal,
  periods periods: Int,
  digits digits: Int,
) -> Result(decimal.Decimal, InterestError)

Simple interest: I = P × r × t.

Search Document