soma.cell

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.

Generated from soma describe on this file.

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

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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)

Builtins used

abs, pow

Source file: /corpus/finance/loan_amortization.cell · examples/corpus/finance/loan_amortization.cell