Commit 26f82a2a1 swapped `computePaymentFactor` from the cancellation-prone
`power(1+r, n) - 1` to `computePowerMinusOneHybrid`, but did not
amendment-gate the swap. Because the change alters the bit-pattern of
amortization output at near-zero rates, applying it without a gate would
fork consensus on any ledger with a long-running near-zero-rate loan.
Gate the hybrid path on `fixCleanup3_2_0`. When the amendment is not
enabled, `computePaymentFactor` falls back to the original direct
subtraction, preserving pre-fix output bit-exactly. When enabled, the
hybrid path is used and the near-zero-rate regression tests
(`testBugInterestDueDeltaCrash`, `testFullLifecycleVaultPnLNearZeroRate`)
pass.
Threads `Rules const&` from the public lending API down to
`computePaymentFactor`:
- `computePaymentFactor`, `loanPeriodicPayment`,
`loanPrincipalFromPeriodicPayment` (leaves)
- `computeTheoreticalLoanState`, `computeFullPayment`,
`computePaymentComponents` (mid-level)
- `computeLoanProperties` (both overloads), `tryOverpayment`,
`doOverpayment` (top-level)
Public callers (`LoanSet::doApply`, `loanMakePayment`) pass
`view.rules()`. Tests pass `env.current()->rules()`.
At near-zero `periodicRate`, the direct subtraction
`power(1 + r, n) - 1` suffers catastrophic cancellation: `(1+r)^n`
rounds to a value very close to 1 and the subtraction discards most
of Number's 19-digit large-mantissa precision. The resulting
amortization factor is inaccurate enough that
`loanPrincipalFromPeriodicPayment` returns a principal greater than
`periodicPayment * paymentsRemaining`, which propagates into
`computeTheoreticalLoanState` as a negative `interestDue` and fires
the `interest due delta not greater than outstanding` assertion in
`computePaymentComponents` (testBugInterestDueDeltaCrash).
The same numerical defect causes systematic underpayment of
interest in release builds at the bug regime. On a $1B loan with
the test parameters, closed-form charges only $0.0588 of interest
versus the mathematically correct $0.3805 (verified independently
via 50-digit Decimal arithmetic). Linear scaling: ~$321 underpaid
per $1T of principal.
Replace `raisedRate - 1` with a hybrid evaluator:
- When `r * paymentsRemaining >= 1e-9`, use the closed-form
`power(1+r, n) - 1`. At Number's 19-digit mantissa this still
retains ~10 sig digits post-subtraction and is ~30-500x faster
than the binomial expansion.
- Below the threshold, expand `(1+r)^n - 1 = sum C(n,k) r^k` with
early termination once terms fall below Number precision.
The hybrid takes the closed-form path at every rate covered by
existing fixtures, so output is bit-identical to pre-fix code at
moderate rates (no fixture drift).
Also drops the now-unused `computeRaisedRate` from the lending
helpers (its only caller was `computePaymentFactor`).
Test coverage:
- testComputePowerMinusOne / testComputePowerMinusOneHybrid:
direct unit tests for both new helpers, including property
checks against `(1+r)^2 = 1 + 2r + r^2` and `(1+r)^3 = 1 + 3r +
3r^2 + r^3`, threshold-boundary verification at exactly
r*n = 1e-9, and large-n early-termination.
- testLoanPrincipalFromPeriodicPaymentNearZeroRate: regression
guard, asserts `principal <= payment * n` at near-zero rate.
- testComputeTheoreticalLoanStateNearZeroRate: regression guard,
asserts `interestDue >= 0` and `principalOutstanding <=
valueOutstanding`.
- testBugInterestDueDeltaCrash: end-to-end reproduction of the
original assertion abort, now passes cleanly.
- testFullLifecycleVaultPnLNearZeroRate: integration test running
a $1B loan to completion, verifies the vault collects the
economically-correct interest matching the 50-digit Decimal
reference within sub-microcent tolerance, plus self-consistency
(vault gain == TVO - principal at LoanSet) and conservation
(borrower outflow == vault gain + broker gain).
This change:
* Removes a set of unnecessary brackets in the initialization of an `std::uint32_t`.
* Fixes a couple of incorrect flags (same value, just wrong variables - so no amendment needed).
This change replaces all instances of `<variable> != tesSUCCESS` with `!isTesSuccess(<variable>)` and `<variable> == tesSUCCESS` with `isTesSuccess(<variable>)`.