Fixed-rate loan amortization: monthly payment, remaining balance after k payments, and total interest paid
Standard annuity formula with periodic compounding.
A Soma program from the finance part of the verified corpus: 48 lines, cells LoanAmortization, LoanTests. It passes soma check with soma 3.2.4.
How it is built
The cell LoanAmortization declares 3 signals in its face: payment(principal: Float, annual_rate: Float, months: Int) -> Float, balance_after(principal: Float, annual_rate: Float, months: Int, k: Int) -> Float and total_interest(principal: Float, annual_rate: Float, months: Int) -> Float. Handlers: payment (line 13), balance_after (line 21) and total_interest (line 29).
The file also holds 1 test cell (LoanTests) with 5 assertions, run by soma test.
The source
loan_amortization.cell
// Fixed-rate loan amortization: monthly payment, remaining balance after
// k payments, and total interest paid. Standard annuity formula with
// periodic compounding.
cell LoanAmortization {
face {
signal payment(principal: Float, annual_rate: Float, months: Int) -> Float
signal balance_after(principal: Float, annual_rate: Float, months: Int, k: Int) -> Float
signal total_interest(principal: Float, annual_rate: Float, months: Int) -> Float
}
// M = P * r(1+r)^n / ((1+r)^n - 1), r = monthly rate
on payment(principal: Float, annual_rate: Float, months: Int) {
let r = annual_rate / 12.0
if r == 0.0 { return principal / months }
let g = pow(1.0 + r, months)
return principal * r * g / (g - 1.0)
}
// remaining balance B_k = P(1+r)^k - M*((1+r)^k - 1)/r
on balance_after(principal: Float, annual_rate: Float, months: Int, k: Int) {
let r = annual_rate / 12.0
let m = payment(principal, annual_rate, months)
if r == 0.0 { return principal - m * k }
let g = pow(1.0 + r, k)
return principal * g - m * (g - 1.0) / r
}
on total_interest(principal: Float, annual_rate: Float, months: Int) {
return payment(principal, annual_rate, months) * months - principal
}
}
cell test LoanTests {
rules {
// 200k @ 6% / 360 months ≈ 1199.10/mo
assert abs(payment(200000.0, 0.06, 360) - 1199.101) < 0.01
// balance after all payments is ~0
assert abs(balance_after(200000.0, 0.06, 360, 360)) < 0.01
// balance after 0 payments is the principal
assert abs(balance_after(200000.0, 0.06, 360, 0) - 200000.0) < 0.01
// total interest is positive and large on a 30y loan
assert total_interest(200000.0, 0.06, 360) > 200000.0
// zero-rate loan: payment is just principal/n
assert abs(payment(1200.0, 0.0, 12) - 100.0) < 0.0001
}
}What the toolchain says
$ soma check loan_amortization.cell
✓ All checks passed.$ soma test loan_amortization.cell
test LoanTests ...
✓ assert abs(payment(200000.0, 0.06, 360) - 1199.101) < 0.01
✓ assert abs(balance_after(200000.0, 0.06, 360, 360)) < 0.01
✓ assert abs(balance_after(200000.0, 0.06, 360, 0) - 200000.0) < 0.01
✓ assert total_interest(200000.0, 0.06, 360) > 200000.0
✓ assert abs(payment(1200.0, 0.0, 12) - 100.0) < 0.0001
5 tests: 5 passed, 0 failed$ soma verify loan_amortization.cell
Verifying loan_amortization.cell...
No state machine in this program: nothing to prove beyond `soma check` (invariants and lifecycles are what verify proves).
VERIFY OK (vacuous — no state machine)