broken_source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start + 1):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: payments before forbearance', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['regression: payments before forbearance, partial-repair probe', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['sample 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 2', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['sample 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['sample 4', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 88417, 'deferred': 0, 'payment': 2291}], ['sample 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}]], [['regression: payments before forbearance', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['sample 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['sample 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['sample 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['sample 4', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['sample 5', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['sample 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: payments before forbearance', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['sample 1', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 859545, 'deferred': 0, 'payment': 49898}], ['sample 2', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['sample 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['sample 4', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 63367, 'deferred': 0, 'payment': 9418}], ['sample 5', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 248449, 'deferred': 0, 'payment': 5895}], ['sample 6', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 238865, 'deferred': 0, 'payment': 4978}]], [['regression: payments before forbearance', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['sample 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['sample 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['sample 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['sample 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['sample 6', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}]], [['regression: payments before forbearance', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['sample 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 2', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['sample 3', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['sample 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['sample 5', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['sample 6', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}]]]
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)

source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(2, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: payments before forbearance', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['regression: payments before forbearance, partial-repair probe', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['sample 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 2', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['sample 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['sample 4', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 88417, 'deferred': 0, 'payment': 2291}], ['sample 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}]], [['regression: payments before forbearance', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['sample 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['sample 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['sample 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['sample 4', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['sample 5', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['sample 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: payments before forbearance', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['sample 1', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 859545, 'deferred': 0, 'payment': 49898}], ['sample 2', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['sample 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['sample 4', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 63367, 'deferred': 0, 'payment': 9418}], ['sample 5', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 248449, 'deferred': 0, 'payment': 5895}], ['sample 6', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 238865, 'deferred': 0, 'payment': 4978}]], [['regression: payments before forbearance', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['sample 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['sample 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['sample 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['sample 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['sample 6', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}]], [['regression: payments before forbearance', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['sample 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['sample 2', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['sample 3', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['sample 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['sample 5', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['sample 6', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}]]]
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)

prompt:
The balance entering forbearance reflects a payment the borrower never made.

x = {'principal', 'rate_bp', 'months' n, 'forbear': [start_month, count], 'mode': 'capitalize'|'defer'}. Months before start_month are paid with the level payment (exact annuity, half-up, interest round_half_up(balance*bp/120000)). During the count forbearance months interest accrues on the unchanged balance, rounded monthly, without compounding. The remaining payment count n - (start - 1) is preserved (term extended). capitalize: accrued interest is added to the balance and the payment re-leveled over the remaining count; defer: accrued interest goes to a non-interest bucket and the payment is unchanged. Return {'balance', 'deferred', 'payment'}.

split: open-access


broken_source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd((bal + acc) * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: forbearance accrual compounding', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['regression: forbearance accrual compounding, partial-repair probe', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}]], [['regression: forbearance accrual compounding', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 5000, 'payment': 19333}], ['control 6', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 20837, 'deferred': 0, 'payment': 20833}]], [['regression: forbearance accrual compounding', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 1], 'mode': 'capitalize'}, {'balance': 958333, 'deferred': 0, 'payment': 41667}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 85843, 'deferred': 858, 'payment': 2224}]], [['regression: forbearance accrual compounding', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 55929, 'deferred': 0, 'payment': 4454}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 927614, 'deferred': 4638, 'payment': 19333}]], [['regression: forbearance accrual compounding', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 541663, 'deferred': 0, 'payment': 41666}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 86070, 'deferred': 0, 'payment': 86500}]]]
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)

meaning: Fixture-count band, not measured model difficulty

deps: stdlib

source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp * (k + 1), 120000) - rnd(bal * bp * k, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: forbearance accrual compounding', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['regression: forbearance accrual compounding, partial-repair probe', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}]], [['regression: forbearance accrual compounding', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 5000, 'payment': 19333}], ['control 6', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 20837, 'deferred': 0, 'payment': 20833}]], [['regression: forbearance accrual compounding', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 1], 'mode': 'capitalize'}, {'balance': 958333, 'deferred': 0, 'payment': 41667}], ['control 6', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 85843, 'deferred': 858, 'payment': 2224}]], [['regression: forbearance accrual compounding', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 55929, 'deferred': 0, 'payment': 4454}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 927614, 'deferred': 4638, 'payment': 19333}]], [['regression: forbearance accrual compounding', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 541663, 'deferred': 0, 'payment': 41666}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 86070, 'deferred': 0, 'payment': 86500}]]]
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)

prompt:
Forbearance interest compounds monthly.

x = {'principal', 'rate_bp', 'months' n, 'forbear': [start_month, count], 'mode': 'capitalize'|'defer'}. Months before start_month are paid with the level payment (exact annuity, half-up, interest round_half_up(balance*bp/120000)). During the count forbearance months interest accrues on the unchanged balance, rounded monthly, without compounding. The remaining payment count n - (start - 1) is preserved (term extended). capitalize: accrued interest is added to the balance and the payment re-leveled over the remaining count; defer: accrued interest goes to a non-interest bucket and the payment is unchanged. Return {'balance', 'deferred', 'payment'}.

split: open-access


broken_source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1) - count
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: extended remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 4', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 197915, 'deferred': 0, 'payment': 10417}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 838336, 'deferred': 25152, 'payment': 19333}]], [['regression: extended remaining term', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: extended remaining term', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 85642, 'deferred': 1284, 'payment': 86066}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 250000, 'deferred': 15000, 'payment': 5561}]], [['regression: extended remaining term', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 2', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 927614, 'deferred': 4638, 'payment': 19333}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 96068, 'deferred': 480, 'payment': 4432}]], [['regression: extended remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 21996, 'deferred': 660, 'payment': 22212}]]]
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)

source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - start
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: extended remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 4', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 197915, 'deferred': 0, 'payment': 10417}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 838336, 'deferred': 25152, 'payment': 19333}]], [['regression: extended remaining term', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: extended remaining term', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 85642, 'deferred': 1284, 'payment': 86066}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 250000, 'deferred': 15000, 'payment': 5561}]], [['regression: extended remaining term', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 2', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 927614, 'deferred': 4638, 'payment': 19333}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 96068, 'deferred': 480, 'payment': 4432}]], [['regression: extended remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 1', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 21996, 'deferred': 660, 'payment': 22212}]]]
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)

prompt:
Capitalized forbearance payments jump because the term was not extended.

x = {'principal', 'rate_bp', 'months' n, 'forbear': [start_month, count], 'mode': 'capitalize'|'defer'}. Months before start_month are paid with the level payment (exact annuity, half-up, interest round_half_up(balance*bp/120000)). During the count forbearance months interest accrues on the unchanged balance, rounded monthly, without compounding. The remaining payment count n - (start - 1) is preserved (term extended). capitalize: accrued interest is added to the balance and the payment re-leveled over the remaining count; defer: accrued interest goes to a non-interest bucket and the payment is unchanged. Return {'balance', 'deferred', 'payment'}.

split: open-access


broken_source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = level(bal + acc, remaining)
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: deferral payment', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 88417, 'deferred': 0, 'payment': 2291}]], [['regression: deferral payment', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 41667}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 79165, 'deferred': 0, 'payment': 4167}]], [['regression: deferral payment', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 2', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 859545, 'deferred': 0, 'payment': 49898}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 63367, 'deferred': 0, 'payment': 9418}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 248449, 'deferred': 0, 'payment': 5895}]], [['regression: deferral payment', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 55929, 'deferred': 0, 'payment': 4454}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 1], 'mode': 'capitalize'}, {'balance': 94694, 'deferred': 0, 'payment': 2247}]], [['regression: deferral payment', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}], ['control 4', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 541663, 'deferred': 0, 'payment': 41666}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [1, 6], 'mode': 'capitalize'}, {'balance': 106000, 'deferred': 0, 'payment': 9418}]]]
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)

source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = level(bal, remaining)
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: deferral payment', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 88417, 'deferred': 0, 'payment': 2291}]], [['regression: deferral payment', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 5', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 41667}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 79165, 'deferred': 0, 'payment': 4167}]], [['regression: deferral payment', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 2', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 859545, 'deferred': 0, 'payment': 49898}], ['control 3', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 101000, 'deferred': 0, 'payment': 2247}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 63367, 'deferred': 0, 'payment': 9418}], ['control 6', {'principal': 250000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 6], 'mode': 'capitalize'}, {'balance': 248449, 'deferred': 0, 'payment': 5895}]], [['regression: deferral payment', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 55929, 'deferred': 0, 'payment': 4454}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 60, 'forbear': [6, 1], 'mode': 'capitalize'}, {'balance': 94694, 'deferred': 0, 'payment': 2247}]], [['regression: deferral payment', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['regression: deferral payment, partial-repair probe', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}], ['control 4', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 3], 'mode': 'capitalize'}, {'balance': 541663, 'deferred': 0, 'payment': 41666}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [1, 6], 'mode': 'capitalize'}, {'balance': 106000, 'deferred': 0, 'payment': 9418}]]]
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)

prompt:
Deferred forbearance raises the monthly payment.

x = {'principal', 'rate_bp', 'months' n, 'forbear': [start_month, count], 'mode': 'capitalize'|'defer'}. Months before start_month are paid with the level payment (exact annuity, half-up, interest round_half_up(balance*bp/120000)). During the count forbearance months interest accrues on the unchanged balance, rounded monthly, without compounding. The remaining payment count n - (start - 1) is preserved (term extended). capitalize: accrued interest is added to the balance and the payment re-leveled over the remaining count; defer: accrued interest goes to a non-interest bucket and the payment is unchanged. Return {'balance', 'deferred', 'payment'}.

split: open-access


broken_source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        new_pay = level(bal, remaining)
        deferred = 0
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: capitalization', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 6', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}]], [['regression: capitalization', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: capitalization', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 85642, 'deferred': 1284, 'payment': 86066}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 6', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 1], 'mode': 'capitalize'}, {'balance': 958333, 'deferred': 0, 'payment': 41667}]], [['regression: capitalization', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}]], [['regression: capitalization', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}]]]
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)

source:
"""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']
    P = x['principal']
    pay = level(P, n)
    bal = P
    start, count = x['forbear']
    for k in range(1, start):
        interest = rnd(bal * bp, 120000)
        bal -= pay - interest
    acc = 0
    for k in range(count):
        acc += rnd(bal * bp, 120000)
    remaining = n - (start - 1)
    if x['mode'] == 'capitalize':
        bal += acc
        new_pay = level(bal, remaining)
        deferred = acc
    else:
        deferred = acc
        new_pay = pay
    return {'balance': bal, 'deferred': deferred, 'payment': new_pay}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: capitalization', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'capitalize'}, {'balance': 102071, 'deferred': 0, 'payment': 4990}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [1, 3], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 4167}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 3], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 54163, 'deferred': 0, 'payment': 4166}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 230288, 'deferred': 13818, 'payment': 22212}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 92115, 'deferred': 921, 'payment': 8885}], ['control 6', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}]], [['regression: capitalization', {'principal': 250000, 'rate_bp': 1200, 'months': 12, 'forbear': [12, 6], 'mode': 'capitalize'}, {'balance': 23316, 'deferred': 0, 'payment': 23549}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 801419, 'deferred': 24042, 'payment': 44321}], ['control 2', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 3', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 21406, 'deferred': 107, 'payment': 21517}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [6, 6], 'mode': 'defer'}, {'balance': 91665, 'deferred': 0, 'payment': 1667}], ['control 5', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 231905, 'deferred': 1160, 'payment': 4833}], ['control 6', {'principal': 250000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 240170, 'deferred': 3603, 'payment': 11080}]], [['regression: capitalization', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 975088, 'deferred': 0, 'payment': 44985}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 100000, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 142798, 'deferred': 4284, 'payment': 11768}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'months': 24, 'forbear': [6, 1], 'mode': 'defer'}, {'balance': 801419, 'deferred': 4007, 'payment': 44321}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 85642, 'deferred': 1284, 'payment': 86066}], ['control 5', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 3], 'mode': 'defer'}, {'balance': 239583, 'deferred': 0, 'payment': 10417}], ['control 6', {'principal': 1000000, 'rate_bp': 0, 'months': 24, 'forbear': [2, 1], 'mode': 'capitalize'}, {'balance': 958333, 'deferred': 0, 'payment': 41667}]], [['regression: capitalization', {'principal': 250000, 'rate_bp': 600, 'months': 60, 'forbear': [6, 3], 'mode': 'capitalize'}, {'balance': 235385, 'deferred': 0, 'payment': 4906}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 1], 'mode': 'capitalize'}, {'balance': 20837, 'deferred': 0, 'payment': 20837}], ['control 2', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'forbear': [1, 1], 'mode': 'defer'}, {'balance': 250000, 'deferred': 1250, 'payment': 21517}], ['control 3', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 1], 'mode': 'capitalize'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 6], 'mode': 'defer'}, {'balance': 962927, 'deferred': 57774, 'payment': 47073}], ['control 5', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'forbear': [12, 6], 'mode': 'defer'}, {'balance': 8337, 'deferred': 0, 'payment': 8333}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}]], [['regression: capitalization', {'principal': 1000000, 'rate_bp': 1200, 'months': 24, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 991814, 'deferred': 0, 'payment': 48486}], ['control 1', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'forbear': [2, 3], 'mode': 'capitalize'}, {'balance': 98333, 'deferred': 0, 'payment': 1667}], ['control 2', {'principal': 1000000, 'rate_bp': 0, 'months': 60, 'forbear': [1, 6], 'mode': 'defer'}, {'balance': 1000000, 'deferred': 0, 'payment': 16667}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 60, 'forbear': [12, 3], 'mode': 'defer'}, {'balance': 83837, 'deferred': 1257, 'payment': 1933}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [6, 3], 'mode': 'defer'}, {'balance': 59058, 'deferred': 885, 'payment': 8607}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'months': 12, 'forbear': [2, 1], 'mode': 'defer'}, {'balance': 918934, 'deferred': 4595, 'payment': 86066}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'forbear': [12, 1], 'mode': 'defer'}, {'balance': 8560, 'deferred': 43, 'payment': 8607}]]]
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)

prompt:
Capitalized forbearance interest never reaches the principal balance.

x = {'principal', 'rate_bp', 'months' n, 'forbear': [start_month, count], 'mode': 'capitalize'|'defer'}. Months before start_month are paid with the level payment (exact annuity, half-up, interest round_half_up(balance*bp/120000)). During the count forbearance months interest accrues on the unchanged balance, rounded monthly, without compounding. The remaining payment count n - (start - 1) is preserved (term extended). capitalize: accrued interest is added to the balance and the payment re-leveled over the remaining count; defer: accrued interest goes to a non-interest bucket and the payment is unchanged. Return {'balance', 'deferred', 'payment'}.

split: open-access


broken_source:
"""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']
    P = x['principal']
    n = x['months']
    pay = level(P, n)
    sched = actual = P
    auto = request = None
    for k in range(1, n + 1):
        sched -= pay - rnd(sched * bp, 120000)
        if actual > 0:
            actual -= pay - rnd(actual * bp, 120000)
            actual -= min(x['prepay'].get(str(k), 0), max(actual, 0))
        if request is None and actual * 100 <= 80 * x['orig_value']:
            request = k
        if auto is None and actual * 100 <= 78 * x['orig_value']:
            auto = k
    mid = n // 2 + 1
    auto = mid if auto is None else min(auto, mid)
    return {'request': request, 'automatic': auto}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: automatic uses scheduled balance', {'principal': 200000, 'rate_bp': 1200, 'months': 120, 'orig_value': 220000, 'prepay': {'11': 5000, '18': 40000}}, {'request': 18, 'automatic': 29}], ['control 1', {'principal': 180000, 'rate_bp': 1200, 'months': 360, 'orig_value': 250000, 'prepay': {'21': 5000}}, {'request': 1, 'automatic': 1}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 24, 'orig_value': 200000, 'prepay': {'6': 20000, '5': 5000}}, {'request': 5, 'automatic': 5}], ['control 3', {'principal': 200000, 'rate_bp': 1200, 'months': 24, 'orig_value': 220000, 'prepay': {}}, {'request': 4, 'automatic': 4}], ['control 4', {'principal': 215000, 'rate_bp': 0, 'months': 120, 'orig_value': 220000, 'prepay': {}}, {'request': 22, 'automatic': 25}], ['control 5', {'principal': 160000, 'rate_bp': 600, 'months': 36, 'orig_value': 205000, 'prepay': {'8': 40000}}, {'request': 1, 'automatic': 1}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'orig_value': 200000, 'prepay': {'6': 40000}}, {'request': 1, 'automatic': 1}]], [['regression: automatic uses scheduled balance', {'principal': 200000, 'rate_bp': 1500, 'months': 360, 'orig_value': 205000, 'prepay': {'24': 5000, '2': 20000}}, {'request': 33, 'automatic': 181}], ['control 1', {'principal': 200000, 'rate_bp': 0, 'months': 24, 'orig_value': 205000, 'prepay': {'5': 20000}}, {'request': 5, 'automatic': 5}], ['control 2', {'principal': 160000, 'rate_bp': 1200, 'months': 48, 'orig_value': 205000, 'prepay': {'7': 40000}}, {'request': 1, 'automatic': 1}], ['control 3', {'principal': 215000, 'rate_bp': 600, 'months': 24, 'orig_value': 200000, 'prepay': {'21': 20000}}, {'request': 7, 'automatic': 7}], ['control 4', {'principal': 199000, 'rate_bp': 600, 'months': 36, 'orig_value': 205000, 'prepay': {'8': 40000, '21': 5000}}, {'request': 7, 'automatic': 8}], ['control 5', {'principal': 190000, 'rate_bp': 0, 'months': 36, 'orig_value': 200000, 'prepay': {}}, {'request': 6, 'automatic': 7}], ['control 6', {'principal': 190000, 'rate_bp': 1200, 'months': 36, 'orig_value': 200000, 'prepay': {'16': 20000}}, {'request': 7, 'automatic': 8}]], [['regression: automatic uses scheduled balance', {'principal': 190000, 'rate_bp': 600, 'months': 48, 'orig_value': 200000, 'prepay': {'6': 20000}}, {'request': 6, 'automatic': 10}], ['control 1', {'principal': 180000, 'rate_bp': 1200, 'months': 60, 'orig_value': 220000, 'prepay': {}}, {'request': 2, 'automatic': 4}], ['control 2', {'principal': 200000, 'rate_bp': 0, 'months': 48, 'orig_value': 220000, 'prepay': {}}, {'request': 6, 'automatic': 7}], ['control 3', {'principal': 160000, 'rate_bp': 1200, 'months': 360, 'orig_value': 250000, 'prepay': {'5': 40000}}, {'request': 1, 'automatic': 1}], ['control 4', {'principal': 215000, 'rate_bp': 1200, 'months': 24, 'orig_value': 205000, 'prepay': {'2': 5000}}, {'request': 6, 'automatic': 7}], ['control 5', {'principal': 215000, 'rate_bp': 600, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 3, 'automatic': 4}], ['control 6', {'principal': 190000, 'rate_bp': 600, 'months': 24, 'orig_value': 205000, 'prepay': {}}, {'request': 4, 'automatic': 4}]], [['regression: automatic uses scheduled balance', {'principal': 199000, 'rate_bp': 1500, 'months': 120, 'orig_value': 200000, 'prepay': {'22': 20000}}, {'request': 23, 'automatic': 45}], ['control 1', {'principal': 200000, 'rate_bp': 0, 'months': 120, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 3}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}], ['control 3', {'principal': 180000, 'rate_bp': 1500, 'months': 24, 'orig_value': 220000, 'prepay': {}}, {'request': 1, 'automatic': 2}], ['control 4', {'principal': 200000, 'rate_bp': 600, 'months': 120, 'orig_value': 205000, 'prepay': {}}, {'request': 28, 'automatic': 31}], ['control 5', {'principal': 215000, 'rate_bp': 600, 'months': 36, 'orig_value': 220000, 'prepay': {}}, {'request': 8, 'automatic': 8}], ['control 6', {'principal': 160000, 'rate_bp': 0, 'months': 120, 'orig_value': 220000, 'prepay': {'4': 5000}}, {'request': 1, 'automatic': 1}]], [['regression: automatic uses scheduled balance', {'principal': 199000, 'rate_bp': 600, 'months': 360, 'orig_value': 200000, 'prepay': {'22': 5000}}, {'request': 116, 'automatic': 148}], ['control 1', {'principal': 199000, 'rate_bp': 1500, 'months': 360, 'orig_value': 220000, 'prepay': {}}, {'request': 194, 'automatic': 181}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 24, 'orig_value': 220000, 'prepay': {'6': 20000}}, {'request': 3, 'automatic': 3}], ['control 3', {'principal': 200000, 'rate_bp': 0, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}], ['control 4', {'principal': 190000, 'rate_bp': 600, 'months': 24, 'orig_value': 200000, 'prepay': {'9': 40000}}, {'request': 4, 'automatic': 5}], ['control 5', {'principal': 180000, 'rate_bp': 1200, 'months': 36, 'orig_value': 250000, 'prepay': {'5': 40000}}, {'request': 1, 'automatic': 1}], ['control 6', {'principal': 180000, 'rate_bp': 1500, 'months': 120, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}]]]
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)

source:
"""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']
    P = x['principal']
    n = x['months']
    pay = level(P, n)
    sched = actual = P
    auto = request = None
    for k in range(1, n + 1):
        sched -= pay - rnd(sched * bp, 120000)
        if actual > 0:
            actual -= pay - rnd(actual * bp, 120000)
            actual -= min(x['prepay'].get(str(k), 0), max(actual, 0))
        if request is None and actual * 100 <= 80 * x['orig_value']:
            request = k
        if auto is None and min(sched, actual) * 100 <= 78 * x['orig_value']:
            auto = k
    mid = n // 2 + 1
    auto = mid if auto is None else min(auto, mid)
    return {'request': request, 'automatic': auto}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: automatic uses scheduled balance', {'principal': 200000, 'rate_bp': 1200, 'months': 120, 'orig_value': 220000, 'prepay': {'11': 5000, '18': 40000}}, {'request': 18, 'automatic': 29}], ['control 1', {'principal': 180000, 'rate_bp': 1200, 'months': 360, 'orig_value': 250000, 'prepay': {'21': 5000}}, {'request': 1, 'automatic': 1}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 24, 'orig_value': 200000, 'prepay': {'6': 20000, '5': 5000}}, {'request': 5, 'automatic': 5}], ['control 3', {'principal': 200000, 'rate_bp': 1200, 'months': 24, 'orig_value': 220000, 'prepay': {}}, {'request': 4, 'automatic': 4}], ['control 4', {'principal': 215000, 'rate_bp': 0, 'months': 120, 'orig_value': 220000, 'prepay': {}}, {'request': 22, 'automatic': 25}], ['control 5', {'principal': 160000, 'rate_bp': 600, 'months': 36, 'orig_value': 205000, 'prepay': {'8': 40000}}, {'request': 1, 'automatic': 1}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 60, 'orig_value': 200000, 'prepay': {'6': 40000}}, {'request': 1, 'automatic': 1}]], [['regression: automatic uses scheduled balance', {'principal': 200000, 'rate_bp': 1500, 'months': 360, 'orig_value': 205000, 'prepay': {'24': 5000, '2': 20000}}, {'request': 33, 'automatic': 181}], ['control 1', {'principal': 200000, 'rate_bp': 0, 'months': 24, 'orig_value': 205000, 'prepay': {'5': 20000}}, {'request': 5, 'automatic': 5}], ['control 2', {'principal': 160000, 'rate_bp': 1200, 'months': 48, 'orig_value': 205000, 'prepay': {'7': 40000}}, {'request': 1, 'automatic': 1}], ['control 3', {'principal': 215000, 'rate_bp': 600, 'months': 24, 'orig_value': 200000, 'prepay': {'21': 20000}}, {'request': 7, 'automatic': 7}], ['control 4', {'principal': 199000, 'rate_bp': 600, 'months': 36, 'orig_value': 205000, 'prepay': {'8': 40000, '21': 5000}}, {'request': 7, 'automatic': 8}], ['control 5', {'principal': 190000, 'rate_bp': 0, 'months': 36, 'orig_value': 200000, 'prepay': {}}, {'request': 6, 'automatic': 7}], ['control 6', {'principal': 190000, 'rate_bp': 1200, 'months': 36, 'orig_value': 200000, 'prepay': {'16': 20000}}, {'request': 7, 'automatic': 8}]], [['regression: automatic uses scheduled balance', {'principal': 190000, 'rate_bp': 600, 'months': 48, 'orig_value': 200000, 'prepay': {'6': 20000}}, {'request': 6, 'automatic': 10}], ['control 1', {'principal': 180000, 'rate_bp': 1200, 'months': 60, 'orig_value': 220000, 'prepay': {}}, {'request': 2, 'automatic': 4}], ['control 2', {'principal': 200000, 'rate_bp': 0, 'months': 48, 'orig_value': 220000, 'prepay': {}}, {'request': 6, 'automatic': 7}], ['control 3', {'principal': 160000, 'rate_bp': 1200, 'months': 360, 'orig_value': 250000, 'prepay': {'5': 40000}}, {'request': 1, 'automatic': 1}], ['control 4', {'principal': 215000, 'rate_bp': 1200, 'months': 24, 'orig_value': 205000, 'prepay': {'2': 5000}}, {'request': 6, 'automatic': 7}], ['control 5', {'principal': 215000, 'rate_bp': 600, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 3, 'automatic': 4}], ['control 6', {'principal': 190000, 'rate_bp': 600, 'months': 24, 'orig_value': 205000, 'prepay': {}}, {'request': 4, 'automatic': 4}]], [['regression: automatic uses scheduled balance', {'principal': 199000, 'rate_bp': 1500, 'months': 120, 'orig_value': 200000, 'prepay': {'22': 20000}}, {'request': 23, 'automatic': 45}], ['control 1', {'principal': 200000, 'rate_bp': 0, 'months': 120, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 3}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}], ['control 3', {'principal': 180000, 'rate_bp': 1500, 'months': 24, 'orig_value': 220000, 'prepay': {}}, {'request': 1, 'automatic': 2}], ['control 4', {'principal': 200000, 'rate_bp': 600, 'months': 120, 'orig_value': 205000, 'prepay': {}}, {'request': 28, 'automatic': 31}], ['control 5', {'principal': 215000, 'rate_bp': 600, 'months': 36, 'orig_value': 220000, 'prepay': {}}, {'request': 8, 'automatic': 8}], ['control 6', {'principal': 160000, 'rate_bp': 0, 'months': 120, 'orig_value': 220000, 'prepay': {'4': 5000}}, {'request': 1, 'automatic': 1}]], [['regression: automatic uses scheduled balance', {'principal': 199000, 'rate_bp': 600, 'months': 360, 'orig_value': 200000, 'prepay': {'22': 5000}}, {'request': 116, 'automatic': 148}], ['control 1', {'principal': 199000, 'rate_bp': 1500, 'months': 360, 'orig_value': 220000, 'prepay': {}}, {'request': 194, 'automatic': 181}], ['control 2', {'principal': 190000, 'rate_bp': 1500, 'months': 24, 'orig_value': 220000, 'prepay': {'6': 20000}}, {'request': 3, 'automatic': 3}], ['control 3', {'principal': 200000, 'rate_bp': 0, 'months': 36, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}], ['control 4', {'principal': 190000, 'rate_bp': 600, 'months': 24, 'orig_value': 200000, 'prepay': {'9': 40000}}, {'request': 4, 'automatic': 5}], ['control 5', {'principal': 180000, 'rate_bp': 1200, 'months': 36, 'orig_value': 250000, 'prepay': {'5': 40000}}, {'request': 1, 'automatic': 1}], ['control 6', {'principal': 180000, 'rate_bp': 1500, 'months': 120, 'orig_value': 250000, 'prepay': {}}, {'request': 1, 'automatic': 1}]]]
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)

prompt:
Automatic termination is triggered early by voluntary prepayments.

x = {'principal', 'rate_bp', 'months' n, 'orig_value', 'prepay': {'k': extra}}. Level payment is the exact annuity half-up; interest round_half_up(balance*bp/120000). The scheduled balance ignores prepayments; the actual balance applies each extra after payment k (capped at the balance). The request month is the first k with actual*100 <= 80*orig_value; the automatic month is the first k with scheduled*100 <= 78*orig_value, but never later than the midpoint month n//2 + 1. Return {'request', 'automatic'}.

split: open-access