FA-58456 / Loan amortization schedules / Open access
Odd first period day count: due day 31 adjustment · case 01
Loans funded mid-month lose a day of interest when the first due date is the 31st.
ROOT CAUSE
A due day of 31 is always moved to 30 regardless of the start day.
VERIFIED REPAIR
Move a due day of 31 to 30 only when the adjusted start day is 30.
Unsuccessful approach: Testing for a start day of 31 after it was already changed to 30 never fires.
Case contract
x = {'principal', 'rate_bp', 'convention': '30/360'|'act/365'|'act/360', 'start': [y,m,d], 'first_due': [y,m,d]}. 30/360: a start day of 31 becomes 30; then a due day of 31 becomes 30 when the (adjusted) start day is 30; days = 360*dy + 30*dm + dd. Actual conventions count calendar days, basis 365 or 360 (365 even in leap years). If days <= 0 return {'error': 'due_not_after_start'}. Interest = round_half_up(P*bp*days/(10000*basis)). Return {'days', 'basis', 'interest'}.
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 datetime
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
y1, m1, d1 = x['start']
y2, m2, d2 = x['first_due']
conv = x['convention']
if conv == '30/360':
if d1 == 31:
d1 = 30
if d2 == 31:
d2 = 30
days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
basis = 360
else:
days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
basis = 365 if conv == 'act/365' else 360
if days <= 0:
return {'error': 'due_not_after_start'}
interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 30], 'first_due': [2025, 7, 31]}, {'days': 330, 'basis': 360, 'interest': 55000}], ['control 1', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 7, 28], 'first_due': [2024, 7, 28]}, {'days': 366, 'basis': 360, 'interest': 628}], ['control 3', {'principal': 12345, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 30], 'first_due': [2025, 10, 30]}, {'days': 365, 'basis': 360, 'interest': 751}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 31, 'basis': 360, 'interest': 1033}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}]], [['regression: due day 31 adjustment', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 8, 31]}, {'days': 46, 'basis': 360, 'interest': 7667}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 100000, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 417}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}]], [['regression: due day 31 adjustment', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 10, 15], 'first_due': [2024, 12, 31]}, {'days': 76, 'basis': 360, 'interest': 1267}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 31], 'first_due': [2023, 3, 31]}, {'days': 60, 'basis': 360, 'interest': 10000}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 3, 29], 'first_due': [2025, 2, 1]}, {'days': 309, 'basis': 360, 'interest': 3760}], ['control 4', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 5', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 8, 29], 'first_due': [2023, 8, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2025, 1, 31]}, {'days': 360, 'basis': 360, 'interest': 4380}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 5, 29], 'first_due': [2023, 5, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 123}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 1], 'first_due': [2024, 10, 31]}, {'days': 30, 'basis': 360, 'interest': 500}], ['control 3', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 4', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 2, 29], 'first_due': [2024, 4, 28]}, {'days': 59, 'basis': 360, 'interest': 74}], ['control 5', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}]]]
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: due day 31 adjustment | {'basis': 360, 'days': 1, 'interest': 12} | {'basis': 360, 'days': 2, 'interest': 24} | Failed |
| regression: due day 31 adjustment, partial-repair probe | {'basis': 360, 'days': 330, 'interest': 55000} | {'basis': 360, 'days': 330, 'interest': 55000} | Passed |
| control 1 | {'basis': 360, 'days': 352, 'interest': 441} | {'basis': 360, 'days': 352, 'interest': 441} | Passed |
| control 2 | {'basis': 360, 'days': 366, 'interest': 628} | {'basis': 360, 'days': 366, 'interest': 628} | Passed |
| control 3 | {'basis': 360, 'days': 365, 'interest': 751} | {'basis': 360, 'days': 365, 'interest': 751} | Passed |
| control 4 | {'basis': 360, 'days': 31, 'interest': 1033} | {'basis': 360, 'days': 31, 'interest': 1033} | Passed |
| control 5 | {'basis': 360, 'days': 75, 'interest': 309} | {'basis': 360, 'days': 75, 'interest': 309} | Passed |
SHA-256 / 2be7fa59c26d5235d9a8474ac16fdd1694a811aa8046e1992a7df0336ffe89b7
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
y1, m1, d1 = x['start']
y2, m2, d2 = x['first_due']
conv = x['convention']
if conv == '30/360':
if d1 == 31:
d1 = 30
if d2 == 31 and d1 == 31:
d2 = 30
days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
basis = 360
else:
days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
basis = 365 if conv == 'act/365' else 360
if days <= 0:
return {'error': 'due_not_after_start'}
interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 30], 'first_due': [2025, 7, 31]}, {'days': 330, 'basis': 360, 'interest': 55000}], ['control 1', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 7, 28], 'first_due': [2024, 7, 28]}, {'days': 366, 'basis': 360, 'interest': 628}], ['control 3', {'principal': 12345, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 30], 'first_due': [2025, 10, 30]}, {'days': 365, 'basis': 360, 'interest': 751}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 31, 'basis': 360, 'interest': 1033}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}]], [['regression: due day 31 adjustment', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 8, 31]}, {'days': 46, 'basis': 360, 'interest': 7667}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 100000, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 417}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}]], [['regression: due day 31 adjustment', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 10, 15], 'first_due': [2024, 12, 31]}, {'days': 76, 'basis': 360, 'interest': 1267}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 31], 'first_due': [2023, 3, 31]}, {'days': 60, 'basis': 360, 'interest': 10000}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 3, 29], 'first_due': [2025, 2, 1]}, {'days': 309, 'basis': 360, 'interest': 3760}], ['control 4', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 5', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 8, 29], 'first_due': [2023, 8, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2025, 1, 31]}, {'days': 360, 'basis': 360, 'interest': 4380}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 5, 29], 'first_due': [2023, 5, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 123}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 1], 'first_due': [2024, 10, 31]}, {'days': 30, 'basis': 360, 'interest': 500}], ['control 3', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 4', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 2, 29], 'first_due': [2024, 4, 28]}, {'days': 59, 'basis': 360, 'interest': 74}], ['control 5', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}]]]
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: due day 31 adjustment | {'basis': 360, 'days': 2, 'interest': 24} | {'basis': 360, 'days': 2, 'interest': 24} | Passed |
| regression: due day 31 adjustment, partial-repair probe | {'basis': 360, 'days': 331, 'interest': 55167} | {'basis': 360, 'days': 330, 'interest': 55000} | Failed |
| control 1 | {'basis': 360, 'days': 352, 'interest': 441} | {'basis': 360, 'days': 352, 'interest': 441} | Passed |
| control 2 | {'basis': 360, 'days': 366, 'interest': 628} | {'basis': 360, 'days': 366, 'interest': 628} | Passed |
| control 3 | {'basis': 360, 'days': 365, 'interest': 751} | {'basis': 360, 'days': 365, 'interest': 751} | Passed |
| control 4 | {'basis': 360, 'days': 31, 'interest': 1033} | {'basis': 360, 'days': 31, 'interest': 1033} | Passed |
| control 5 | {'basis': 360, 'days': 75, 'interest': 309} | {'basis': 360, 'days': 75, 'interest': 309} | Passed |
SHA-256 / 3bc4b932681c63485b3039043e5116a924760086111cb7f682ab42db5a1ec8e8
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
def rnd(n, d):
q, r = divmod(n, d)
return q + (1 if 2 * r >= d else 0)
y1, m1, d1 = x['start']
y2, m2, d2 = x['first_due']
conv = x['convention']
if conv == '30/360':
if d1 == 31:
d1 = 30
if d2 == 31 and d1 == 30:
d2 = 30
days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
basis = 360
else:
days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
basis = 365 if conv == 'act/365' else 360
if days <= 0:
return {'error': 'due_not_after_start'}
interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 30], 'first_due': [2025, 7, 31]}, {'days': 330, 'basis': 360, 'interest': 55000}], ['control 1', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 7, 28], 'first_due': [2024, 7, 28]}, {'days': 366, 'basis': 360, 'interest': 628}], ['control 3', {'principal': 12345, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 30], 'first_due': [2025, 10, 30]}, {'days': 365, 'basis': 360, 'interest': 751}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 31, 'basis': 360, 'interest': 1033}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}]], [['regression: due day 31 adjustment', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 8, 31]}, {'days': 46, 'basis': 360, 'interest': 7667}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 100000, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 417}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}]], [['regression: due day 31 adjustment', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 10, 15], 'first_due': [2024, 12, 31]}, {'days': 76, 'basis': 360, 'interest': 1267}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 31], 'first_due': [2023, 3, 31]}, {'days': 60, 'basis': 360, 'interest': 10000}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 3, 29], 'first_due': [2025, 2, 1]}, {'days': 309, 'basis': 360, 'interest': 3760}], ['control 4', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 5', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 8, 29], 'first_due': [2023, 8, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2025, 1, 31]}, {'days': 360, 'basis': 360, 'interest': 4380}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}]], [['regression: due day 31 adjustment', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 5, 29], 'first_due': [2023, 5, 31]}, {'days': 2, 'basis': 360, 'interest': 12}], ['regression: due day 31 adjustment, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 31]}, {'days': 30, 'basis': 360, 'interest': 123}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 1], 'first_due': [2024, 10, 31]}, {'days': 30, 'basis': 360, 'interest': 500}], ['control 3', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 4', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 2, 29], 'first_due': [2024, 4, 28]}, {'days': 59, 'basis': 360, 'interest': 74}], ['control 5', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}]]]
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: due day 31 adjustment | {'basis': 360, 'days': 2, 'interest': 24} | {'basis': 360, 'days': 2, 'interest': 24} | Passed |
| regression: due day 31 adjustment, partial-repair probe | {'basis': 360, 'days': 330, 'interest': 55000} | {'basis': 360, 'days': 330, 'interest': 55000} | Passed |
| control 1 | {'basis': 360, 'days': 352, 'interest': 441} | {'basis': 360, 'days': 352, 'interest': 441} | Passed |
| control 2 | {'basis': 360, 'days': 366, 'interest': 628} | {'basis': 360, 'days': 366, 'interest': 628} | Passed |
| control 3 | {'basis': 360, 'days': 365, 'interest': 751} | {'basis': 360, 'days': 365, 'interest': 751} | Passed |
| control 4 | {'basis': 360, 'days': 31, 'interest': 1033} | {'basis': 360, 'days': 31, 'interest': 1033} | Passed |
| control 5 | {'basis': 360, 'days': 75, 'interest': 309} | {'basis': 360, 'days': 75, 'interest': 309} | Passed |
SHA-256 / 85388b6f2631c16fd404437354531daaf46a5651efde9c94d47f5e5467589ae1
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.860447+00:00.
Case digest / 0479bbb799e699068053db3381ec3fc06f67f416f7e0ddfe956a46ab23fd71d3