FA-58421 / Loan amortization schedules / Open access
Principal curtailment with recast or term shortening: prepayment month key · case 01
Extra principal is applied one month after the borrower sent it.
ROOT CAUSE
Prepayments are looked up with a zero-based month index.
VERIFIED REPAIR
Look up the extra for payment number k (one-based).
Unsuccessful approach: Shifting the lookup the other way applies extras a month early.
Case contract
x = {'principal', 'rate_bp', 'months', 'prepay': {'k': extra cents after payment k}, 'mode': 'shorten'|'recast'}. The level payment is the exact annuity rounded half-up. Each month: interest = round_half_up(balance*bp/120000); if balance + interest <= payment or it is month n, pay balance + interest and stop. Otherwise reduce the balance by payment - interest, then apply the extra (capped at the balance). In recast mode a nonzero extra that leaves a balance re-levels the payment over the n - k remaining months. An extra that clears the balance ends the loan with final payment = payment + extra. Return {'months_paid', 'total_interest', 'final_payment', 'payments': payment history}.
Why this case matters
Amortization engines drive borrower statements, payoff quotes and investor remittances; a misplaced rounding step, boundary or ordering rule compounds across hundreds of periods.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
def level(P, n):
if n <= 0 or P <= 0:
return 0
if bp == 0:
exact = Fraction(P, n)
else:
r = Fraction(bp, 120000)
exact = P * r / (1 - (1 + r) ** -n)
return math.floor(exact + Fraction(1, 2))
bp = x['rate_bp']
n = x['months']
bal = x['principal']
pay = level(bal, n)
history = [pay]
total = 0
k = 0
final = 0
while bal > 0 and k < n:
k += 1
interest = rnd(bal * bp, 120000)
total += interest
if bal + interest <= pay or k == n:
final = bal + interest
bal = 0
break
bal -= pay - interest
extra = min(x['prepay'].get(str(k - 1), 0), bal)
if extra:
bal -= extra
if x['mode'] == 'recast' and bal > 0:
pay = level(bal, n - k)
history.append(pay)
if bal == 0:
final = pay + extra
return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: prepayment month key', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 1', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}], ['control 3', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 5712, 'final_payment': 1837, 'payments': [1825]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16665, 'payments': [16667]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 6619, 'final_payment': 8884, 'payments': [8885]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 200000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 2', {'principal': 60000, 'rate_bp': 600, 'months': 6, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1055, 'final_payment': 10175, 'payments': [10176]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 6371, 'final_payment': 4435, 'payments': [4432]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 7789, 'final_payment': 2837, 'payments': [2824]}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 23797, 'final_payment': 7622, 'payments': [7605]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 2000000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 11744, 'final_payment': 1989, 'payments': [1993]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '22': 200000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 6, 'prepay': {'6': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1757, 'final_payment': 16957, 'payments': [16960]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'9': 1000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 4000, 'payments': [5000]}]]]
for label, args, expected in fixtures[N-1]:
try:
actual = solve(args)
except Exception as exc:
actual = 'raised ' + type(exc).__name__
check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: prepayment month key | {'final_payment': 39996, 'months_paid': 13, 'payments': [1667], 'total_interest': 0} | {'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0} | Failed |
| control 1 | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | Passed |
| control 2 | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | Passed |
| control 3 | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | Passed |
| control 4 | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | Passed |
| control 5 | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | Passed |
| control 6 | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | Passed |
SHA-256 / 5db72f5e5d3e365a55c41021b8ea8e2e558f75565641a78f517b4046be7e624a
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
def level(P, n):
if n <= 0 or P <= 0:
return 0
if bp == 0:
exact = Fraction(P, n)
else:
r = Fraction(bp, 120000)
exact = P * r / (1 - (1 + r) ** -n)
return math.floor(exact + Fraction(1, 2))
bp = x['rate_bp']
n = x['months']
bal = x['principal']
pay = level(bal, n)
history = [pay]
total = 0
k = 0
final = 0
while bal > 0 and k < n:
k += 1
interest = rnd(bal * bp, 120000)
total += interest
if bal + interest <= pay or k == n:
final = bal + interest
bal = 0
break
bal -= pay - interest
extra = min(x['prepay'].get(str(k + 1), 0), bal)
if extra:
bal -= extra
if x['mode'] == 'recast' and bal > 0:
pay = level(bal, n - k)
history.append(pay)
if bal == 0:
final = pay + extra
return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: prepayment month key', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 1', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}], ['control 3', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 5712, 'final_payment': 1837, 'payments': [1825]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16665, 'payments': [16667]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 6619, 'final_payment': 8884, 'payments': [8885]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 200000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 2', {'principal': 60000, 'rate_bp': 600, 'months': 6, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1055, 'final_payment': 10175, 'payments': [10176]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 6371, 'final_payment': 4435, 'payments': [4432]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 7789, 'final_payment': 2837, 'payments': [2824]}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 23797, 'final_payment': 7622, 'payments': [7605]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 2000000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 11744, 'final_payment': 1989, 'payments': [1993]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '22': 200000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 6, 'prepay': {'6': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1757, 'final_payment': 16957, 'payments': [16960]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'9': 1000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 4000, 'payments': [5000]}]]]
for label, args, expected in fixtures[N-1]:
try:
actual = solve(args)
except Exception as exc:
actual = 'raised ' + type(exc).__name__
check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: prepayment month key | {'final_payment': 43330, 'months_paid': 11, 'payments': [1667], 'total_interest': 0} | {'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0} | Failed |
| control 1 | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | Passed |
| control 2 | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | Passed |
| control 3 | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | Passed |
| control 4 | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | Passed |
| control 5 | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | Passed |
| control 6 | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | Passed |
SHA-256 / b49f873e016a0d6da5632d6644b855c6417632a9c91e523f947379c0afd27cf9
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
def level(P, n):
if n <= 0 or P <= 0:
return 0
if bp == 0:
exact = Fraction(P, n)
else:
r = Fraction(bp, 120000)
exact = P * r / (1 - (1 + r) ** -n)
return math.floor(exact + Fraction(1, 2))
bp = x['rate_bp']
n = x['months']
bal = x['principal']
pay = level(bal, n)
history = [pay]
total = 0
k = 0
final = 0
while bal > 0 and k < n:
k += 1
interest = rnd(bal * bp, 120000)
total += interest
if bal + interest <= pay or k == n:
final = bal + interest
bal = 0
break
bal -= pay - interest
extra = min(x['prepay'].get(str(k), 0), bal)
if extra:
bal -= extra
if x['mode'] == 'recast' and bal > 0:
pay = level(bal, n - k)
history.append(pay)
if bal == 0:
final = pay + extra
return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: prepayment month key', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 1', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}], ['control 3', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 5712, 'final_payment': 1837, 'payments': [1825]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16665, 'payments': [16667]}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 6619, 'final_payment': 8884, 'payments': [8885]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 200000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}], ['control 2', {'principal': 60000, 'rate_bp': 600, 'months': 6, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1055, 'final_payment': 10175, 'payments': [10176]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 6371, 'final_payment': 4435, 'payments': [4432]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 7789, 'final_payment': 2837, 'payments': [2824]}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 23797, 'final_payment': 7622, 'payments': [7605]}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 6, 'prepay': {'6': 2000000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 3528, 'final_payment': 17253, 'payments': [17255]}]], [['regression: prepayment month key', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 11744, 'final_payment': 1989, 'payments': [1993]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '22': 200000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 6, 'prepay': {'6': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 1757, 'final_payment': 16957, 'payments': [16960]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'9': 1000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 4000, 'payments': [5000]}]]]
for label, args, expected in fixtures[N-1]:
try:
actual = solve(args)
except Exception as exc:
actual = 'raised ' + type(exc).__name__
check(label, actual, expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: prepayment month key | {'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0} | {'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0} | Passed |
| control 1 | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | {'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442} | Passed |
| control 2 | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | {'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0} | Passed |
| control 3 | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | {'final_payment': 1837, 'months_paid': 36, 'payments': [1825], 'total_interest': 5712} | Passed |
| control 4 | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | {'final_payment': 16665, 'months_paid': 6, 'payments': [16667], 'total_interest': 0} | Passed |
| control 5 | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | {'final_payment': 8337, 'months_paid': 12, 'payments': [8333], 'total_interest': 0} | Passed |
| control 6 | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | {'final_payment': 8884, 'months_paid': 12, 'payments': [8885], 'total_interest': 6619} | Passed |
SHA-256 / fb2a03344964078fc7880c7829f2b5375869b329300d83b1c8f6780749ca45c3
Verification & scope
A deterministic bounded teaching model with stipulated toy lending rules stated in the contract; money is integer cents and rates are basis points; it makes no claim of conformance to any regulation, servicing standard or product. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:46:26.529097+00:00.
Case digest / 1b442d63a3d6f07abd023dc75ec3f5cf83b2f74c2f74c73129ab6108479978a8