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:
| Scenario | Textbook | finanza | Drift |
|---|---|---|---|
| PMT 200_000 @ 4%/12 monthly, n=180, dg=2 | 1479.38 | 1479.34 | 0.04 |
| PMT 50_000 @ 3.5%/12 monthly, n=84, dg=2 | 671.99 | 672.01 | 0.02 |
| EAR 5% nominal / daily (365), dg=6 | 0.051267 | 0.051272 | 5 ppm |
| FV 1000 @ 5% for 10 periods, dg=6 | 1628.894627 | 1628.894000 | 0.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
-
NegativePrincipalprincipalwas negative. -
NegativeRateratewas negative. -
PeriodsOutOfRangeperiodswas zero or negative, or exceeded the supported range1..=1200(100 years of monthly compounding). -
CompoundsOutOfRangecompounds_per_yearwas zero or negative. -
NegativeDigitsdigitswas 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.