FA-58476 / Loan amortization schedules / Open access
Odd first period day count: non-positive period guard · case 01
A 30/360 first period that collapses to zero days returns zero interest instead of an error.
ROOT CAUSE
Only negative day counts are rejected.
VERIFIED REPAIR
Reject any first period with zero or negative days.
Unsuccessful approach: Exempting 30/360 lets its zero-day periods through.
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 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: non-positive period guard', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 4, 30], 'first_due': [2024, 4, 30]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 11, 30]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 1], 'first_due': [2024, 12, 1]}, {'days': 30, 'basis': 360, 'interest': 3042}]], [['regression: non-positive period guard', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 5, 31], 'first_due': [2024, 5, 15]}, {'error': 'due_not_after_start'}], ['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': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard | {'basis': 365, 'days': 0, 'interest': 0} | {'error': 'due_not_after_start'} | Failed |
| regression: non-positive period guard, partial-repair probe | {'basis': 360, 'days': 0, 'interest': 0} | {'error': 'due_not_after_start'} | 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 / 9aa831b3d610bf9465b2eb65586d4e5d33bf3958bffef54bddee504a3c64f8e1
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 == 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 and conv != '30/360':
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: non-positive period guard', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 4, 30], 'first_due': [2024, 4, 30]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 11, 30]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 1], 'first_due': [2024, 12, 1]}, {'days': 30, 'basis': 360, 'interest': 3042}]], [['regression: non-positive period guard', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 5, 31], 'first_due': [2024, 5, 15]}, {'error': 'due_not_after_start'}], ['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': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard | {'error': 'due_not_after_start'} | {'error': 'due_not_after_start'} | Passed |
| regression: non-positive period guard, partial-repair probe | {'basis': 360, 'days': 0, 'interest': 0} | {'error': 'due_not_after_start'} | 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 / acc4c2e7643016be575eb2a551420279c80d7c8103c03006324f77d3543a62e8
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: non-positive period guard', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 4, 30], 'first_due': [2024, 4, 30]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 11, 30]}, {'error': 'due_not_after_start'}], ['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}], ['control 6', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 1], 'first_due': [2024, 12, 1]}, {'days': 30, 'basis': 360, 'interest': 3042}]], [['regression: non-positive period guard', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 5, 31], 'first_due': [2024, 5, 15]}, {'error': 'due_not_after_start'}], ['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': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['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: non-positive period guard | {'error': 'due_not_after_start'} | {'error': 'due_not_after_start'} | Passed |
| regression: non-positive period guard, partial-repair probe | {'error': 'due_not_after_start'} | {'error': 'due_not_after_start'} | 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 / 212828c98ca72a996651e3609c2cdffda7c2a369ffe48f637b5f8d8c6199fb82
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:27.134730+00:00.
Case digest / 17ee078edd818c6939bfa4e029e03b3911338103e7f37908c7f7bf854ad4e757