FAILURE MAP
← Case archive

FA-61216 / Bond day-count conventions / Open access

Irregular final coupon amount: a short final period pays a full regular coupon · case 01

Bonds with a short last period overpay the final coupon.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The final coupon is always treated as a regular period coupon.

VERIFIED REPAIR

Pro-rate a short final coupon by days(prev, maturity) over the notional period length.

Unsuccessful approach: Adding one day to the notional period length pro-rates against the wrong denominator.

Case contract

Inputs the last regular coupon date prev, maturity (prev < maturity <= prev + 2 periods), months per period and annual rate. Quasi dates are prev shifted forward k*months with the prev day clamped to month length. c = 100*rate/freq. Short final period (maturity <= q1): c*days(prev,mat)/days(prev,q1). Long final period: c*(1 + days(q1,mat)/days(q1,q2)). Round to 6 decimals.

Why this case matters

Bond accrual and pricing systems depend on exact day-count arithmetic; a single-day error changes settlement cash.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(prev, maturity, months, rate):
    def mlen(y, m):
        if m == 2:
            return 29 if (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 else 28
        return 30 if m in (4, 6, 9, 11) else 31
    P = datetime.date(*prev)
    M = datetime.date(*maturity)
    freq = 12 // months
    def fwd(k):
        t = P.year * 12 + P.month - 1 + k * months
        y, m = t // 12, t % 12 + 1
        return datetime.date(y, m, min(P.day, mlen(y, m)))
    c = 100 * Fraction(str(rate)) / freq
    q1 = fwd(1)
    if M <= q1:
        frac = Fraction(1)
    else:
        q2 = fwd(2)
        frac = 1 + Fraction((M - q1).days, (q2 - q1).days)
    return round(float(c * frac), 6)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression short final stub 1', [[2013, 9, 23], [2014, 3, 12], 6, 0.03], 1.40884], ['regression short final stub 2', [[2042, 5, 10], [2042, 5, 27], 6, 0.0725], 0.334918], ['partial repair probe 1', [[2015, 10, 31], [2016, 1, 31], 3, 0.045], 1.125], ['partial repair probe 2', [[2020, 5, 23], [2020, 11, 23], 6, 0.0725], 3.625], ['normal control 1', [[2018, 2, 28], [2018, 7, 19], 3, 0.03], 1.173913], ['normal control 2', [[2028, 2, 21], [2029, 5, 25], 12, 0.045], 5.646575], ['normal control 3', [[2014, 3, 1], [2014, 10, 2], 6, 0.05], 2.928177], ['normal control 4', [[2007, 7, 17], [2007, 8, 28], 1, 0.05], 0.564516]], [['regression short final stub 1', [[2045, 11, 25], [2045, 11, 26], 1, 0.05], 0.013889], ['regression short final stub 2', [[2044, 5, 31], [2044, 6, 29], 3, 0.03], 0.236413], ['partial repair probe 1', [[2014, 2, 28], [2014, 8, 28], 6, 0.05], 2.5], ['partial repair probe 2', [[2024, 8, 24], [2025, 8, 24], 12, 0.0725], 7.25], ['normal control 1', [[2007, 2, 22], [2007, 3, 31], 1, 0.045], 0.483871], ['normal control 2', [[2017, 10, 31], [2017, 12, 21], 1, 0.05], 0.698925], ['normal control 3', [[2038, 1, 22], [2038, 12, 2], 6, 0.03], 2.584239], ['normal control 4', [[2025, 9, 29], [2025, 11, 22], 1, 0.05], 0.739247]], [['regression short final stub 1', [[2041, 3, 31], [2041, 4, 17], 3, 0.0725], 0.338599], ['regression short final stub 2', [[2024, 10, 27], [2024, 11, 1], 1, 0.0725], 0.097446], ['partial repair probe 1', [[2013, 12, 31], [2014, 6, 30], 6, 0.03], 1.5], ['partial repair probe 2', [[2045, 1, 31], [2045, 2, 28], 1, 0.0725], 0.604167], ['normal control 1', [[2018, 6, 17], [2018, 7, 28], 1, 0.03], 0.33871], ['normal control 2', [[2027, 1, 3], [2027, 10, 30], 6, 0.045], 3.705163], ['normal control 3', [[2041, 2, 11], [2041, 4, 5], 1, 0.0725], 1.091398], ['normal control 4', [[2033, 1, 31], [2034, 1, 1], 6, 0.0725], 6.658967]], [['regression short final stub 1', [[2006, 7, 31], [2006, 9, 19], 6, 0.0725], 0.985054], ['regression short final stub 2', [[2013, 12, 16], [2014, 1, 7], 6, 0.03], 0.181319], ['partial repair probe 1', [[2018, 3, 2], [2019, 3, 2], 12, 0.05], 5.0], ['partial repair probe 2', [[2026, 3, 6], [2026, 4, 6], 1, 0.03], 0.25], ['normal control 1', [[2006, 6, 22], [2007, 1, 17], 6, 0.045], 2.571429], ['normal control 2', [[2034, 12, 27], [2035, 2, 24], 1, 0.0725], 1.149866], ['normal control 3', [[2045, 11, 21], [2046, 5, 8], 3, 0.05], 2.317416], ['normal control 4', [[2043, 1, 17], [2043, 3, 16], 1, 0.05], 0.818452]], [['regression short final stub 1', [[2012, 10, 28], [2012, 11, 25], 6, 0.0725], 0.557692], ['regression short final stub 2', [[2045, 8, 30], [2045, 9, 13], 3, 0.03], 0.11413], ['partial repair probe 1', [[2035, 1, 30], [2035, 7, 30], 6, 0.045], 2.25], ['partial repair probe 2', [[2015, 6, 30], [2016, 6, 30], 12, 0.03], 3.0], ['normal control 1', [[2018, 2, 28], [2018, 7, 27], 3, 0.045], 1.858696], ['normal control 2', [[2030, 12, 29], [2032, 6, 14], 12, 0.0725], 10.577869], ['normal control 3', [[2026, 3, 30], [2027, 6, 18], 12, 0.05], 6.092896], ['normal control 4', [[2038, 12, 13], [2039, 5, 4], 3, 0.05], 1.956522]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression short final stub 11.51.40884Failed
regression short final stub 23.6250.334918Failed
partial repair probe 11.1251.125Passed
partial repair probe 23.6253.625Passed
normal control 11.1739131.173913Passed
normal control 25.6465755.646575Passed
normal control 32.9281772.928177Passed
normal control 40.5645160.564516Passed

SHA-256 / c705a878ee217134fad2a7c75ba2a101ba492e3dcf19f8967b5edf0bcd7de1ea

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(prev, maturity, months, rate):
    def mlen(y, m):
        if m == 2:
            return 29 if (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 else 28
        return 30 if m in (4, 6, 9, 11) else 31
    P = datetime.date(*prev)
    M = datetime.date(*maturity)
    freq = 12 // months
    def fwd(k):
        t = P.year * 12 + P.month - 1 + k * months
        y, m = t // 12, t % 12 + 1
        return datetime.date(y, m, min(P.day, mlen(y, m)))
    c = 100 * Fraction(str(rate)) / freq
    q1 = fwd(1)
    if M <= q1:
        frac = Fraction((M - P).days, (q1 - P).days + 1)
    else:
        q2 = fwd(2)
        frac = 1 + Fraction((M - q1).days, (q2 - q1).days)
    return round(float(c * frac), 6)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression short final stub 1', [[2013, 9, 23], [2014, 3, 12], 6, 0.03], 1.40884], ['regression short final stub 2', [[2042, 5, 10], [2042, 5, 27], 6, 0.0725], 0.334918], ['partial repair probe 1', [[2015, 10, 31], [2016, 1, 31], 3, 0.045], 1.125], ['partial repair probe 2', [[2020, 5, 23], [2020, 11, 23], 6, 0.0725], 3.625], ['normal control 1', [[2018, 2, 28], [2018, 7, 19], 3, 0.03], 1.173913], ['normal control 2', [[2028, 2, 21], [2029, 5, 25], 12, 0.045], 5.646575], ['normal control 3', [[2014, 3, 1], [2014, 10, 2], 6, 0.05], 2.928177], ['normal control 4', [[2007, 7, 17], [2007, 8, 28], 1, 0.05], 0.564516]], [['regression short final stub 1', [[2045, 11, 25], [2045, 11, 26], 1, 0.05], 0.013889], ['regression short final stub 2', [[2044, 5, 31], [2044, 6, 29], 3, 0.03], 0.236413], ['partial repair probe 1', [[2014, 2, 28], [2014, 8, 28], 6, 0.05], 2.5], ['partial repair probe 2', [[2024, 8, 24], [2025, 8, 24], 12, 0.0725], 7.25], ['normal control 1', [[2007, 2, 22], [2007, 3, 31], 1, 0.045], 0.483871], ['normal control 2', [[2017, 10, 31], [2017, 12, 21], 1, 0.05], 0.698925], ['normal control 3', [[2038, 1, 22], [2038, 12, 2], 6, 0.03], 2.584239], ['normal control 4', [[2025, 9, 29], [2025, 11, 22], 1, 0.05], 0.739247]], [['regression short final stub 1', [[2041, 3, 31], [2041, 4, 17], 3, 0.0725], 0.338599], ['regression short final stub 2', [[2024, 10, 27], [2024, 11, 1], 1, 0.0725], 0.097446], ['partial repair probe 1', [[2013, 12, 31], [2014, 6, 30], 6, 0.03], 1.5], ['partial repair probe 2', [[2045, 1, 31], [2045, 2, 28], 1, 0.0725], 0.604167], ['normal control 1', [[2018, 6, 17], [2018, 7, 28], 1, 0.03], 0.33871], ['normal control 2', [[2027, 1, 3], [2027, 10, 30], 6, 0.045], 3.705163], ['normal control 3', [[2041, 2, 11], [2041, 4, 5], 1, 0.0725], 1.091398], ['normal control 4', [[2033, 1, 31], [2034, 1, 1], 6, 0.0725], 6.658967]], [['regression short final stub 1', [[2006, 7, 31], [2006, 9, 19], 6, 0.0725], 0.985054], ['regression short final stub 2', [[2013, 12, 16], [2014, 1, 7], 6, 0.03], 0.181319], ['partial repair probe 1', [[2018, 3, 2], [2019, 3, 2], 12, 0.05], 5.0], ['partial repair probe 2', [[2026, 3, 6], [2026, 4, 6], 1, 0.03], 0.25], ['normal control 1', [[2006, 6, 22], [2007, 1, 17], 6, 0.045], 2.571429], ['normal control 2', [[2034, 12, 27], [2035, 2, 24], 1, 0.0725], 1.149866], ['normal control 3', [[2045, 11, 21], [2046, 5, 8], 3, 0.05], 2.317416], ['normal control 4', [[2043, 1, 17], [2043, 3, 16], 1, 0.05], 0.818452]], [['regression short final stub 1', [[2012, 10, 28], [2012, 11, 25], 6, 0.0725], 0.557692], ['regression short final stub 2', [[2045, 8, 30], [2045, 9, 13], 3, 0.03], 0.11413], ['partial repair probe 1', [[2035, 1, 30], [2035, 7, 30], 6, 0.045], 2.25], ['partial repair probe 2', [[2015, 6, 30], [2016, 6, 30], 12, 0.03], 3.0], ['normal control 1', [[2018, 2, 28], [2018, 7, 27], 3, 0.045], 1.858696], ['normal control 2', [[2030, 12, 29], [2032, 6, 14], 12, 0.0725], 10.577869], ['normal control 3', [[2026, 3, 30], [2027, 6, 18], 12, 0.05], 6.092896], ['normal control 4', [[2038, 12, 13], [2039, 5, 4], 3, 0.05], 1.956522]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression short final stub 11.4010991.40884Failed
regression short final stub 20.3331080.334918Failed
partial repair probe 11.1129031.125Failed
partial repair probe 23.6054053.625Failed
normal control 11.1739131.173913Passed
normal control 25.6465755.646575Passed
normal control 32.9281772.928177Passed
normal control 40.5645160.564516Passed

SHA-256 / d6a18180664763457d19e6a4b4be10834bc9ec97a47a80757170804b3955f4d9

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(prev, maturity, months, rate):
    def mlen(y, m):
        if m == 2:
            return 29 if (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 else 28
        return 30 if m in (4, 6, 9, 11) else 31
    P = datetime.date(*prev)
    M = datetime.date(*maturity)
    freq = 12 // months
    def fwd(k):
        t = P.year * 12 + P.month - 1 + k * months
        y, m = t // 12, t % 12 + 1
        return datetime.date(y, m, min(P.day, mlen(y, m)))
    c = 100 * Fraction(str(rate)) / freq
    q1 = fwd(1)
    if M <= q1:
        frac = Fraction((M - P).days, (q1 - P).days)
    else:
        q2 = fwd(2)
        frac = 1 + Fraction((M - q1).days, (q2 - q1).days)
    return round(float(c * frac), 6)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression short final stub 1', [[2013, 9, 23], [2014, 3, 12], 6, 0.03], 1.40884], ['regression short final stub 2', [[2042, 5, 10], [2042, 5, 27], 6, 0.0725], 0.334918], ['partial repair probe 1', [[2015, 10, 31], [2016, 1, 31], 3, 0.045], 1.125], ['partial repair probe 2', [[2020, 5, 23], [2020, 11, 23], 6, 0.0725], 3.625], ['normal control 1', [[2018, 2, 28], [2018, 7, 19], 3, 0.03], 1.173913], ['normal control 2', [[2028, 2, 21], [2029, 5, 25], 12, 0.045], 5.646575], ['normal control 3', [[2014, 3, 1], [2014, 10, 2], 6, 0.05], 2.928177], ['normal control 4', [[2007, 7, 17], [2007, 8, 28], 1, 0.05], 0.564516]], [['regression short final stub 1', [[2045, 11, 25], [2045, 11, 26], 1, 0.05], 0.013889], ['regression short final stub 2', [[2044, 5, 31], [2044, 6, 29], 3, 0.03], 0.236413], ['partial repair probe 1', [[2014, 2, 28], [2014, 8, 28], 6, 0.05], 2.5], ['partial repair probe 2', [[2024, 8, 24], [2025, 8, 24], 12, 0.0725], 7.25], ['normal control 1', [[2007, 2, 22], [2007, 3, 31], 1, 0.045], 0.483871], ['normal control 2', [[2017, 10, 31], [2017, 12, 21], 1, 0.05], 0.698925], ['normal control 3', [[2038, 1, 22], [2038, 12, 2], 6, 0.03], 2.584239], ['normal control 4', [[2025, 9, 29], [2025, 11, 22], 1, 0.05], 0.739247]], [['regression short final stub 1', [[2041, 3, 31], [2041, 4, 17], 3, 0.0725], 0.338599], ['regression short final stub 2', [[2024, 10, 27], [2024, 11, 1], 1, 0.0725], 0.097446], ['partial repair probe 1', [[2013, 12, 31], [2014, 6, 30], 6, 0.03], 1.5], ['partial repair probe 2', [[2045, 1, 31], [2045, 2, 28], 1, 0.0725], 0.604167], ['normal control 1', [[2018, 6, 17], [2018, 7, 28], 1, 0.03], 0.33871], ['normal control 2', [[2027, 1, 3], [2027, 10, 30], 6, 0.045], 3.705163], ['normal control 3', [[2041, 2, 11], [2041, 4, 5], 1, 0.0725], 1.091398], ['normal control 4', [[2033, 1, 31], [2034, 1, 1], 6, 0.0725], 6.658967]], [['regression short final stub 1', [[2006, 7, 31], [2006, 9, 19], 6, 0.0725], 0.985054], ['regression short final stub 2', [[2013, 12, 16], [2014, 1, 7], 6, 0.03], 0.181319], ['partial repair probe 1', [[2018, 3, 2], [2019, 3, 2], 12, 0.05], 5.0], ['partial repair probe 2', [[2026, 3, 6], [2026, 4, 6], 1, 0.03], 0.25], ['normal control 1', [[2006, 6, 22], [2007, 1, 17], 6, 0.045], 2.571429], ['normal control 2', [[2034, 12, 27], [2035, 2, 24], 1, 0.0725], 1.149866], ['normal control 3', [[2045, 11, 21], [2046, 5, 8], 3, 0.05], 2.317416], ['normal control 4', [[2043, 1, 17], [2043, 3, 16], 1, 0.05], 0.818452]], [['regression short final stub 1', [[2012, 10, 28], [2012, 11, 25], 6, 0.0725], 0.557692], ['regression short final stub 2', [[2045, 8, 30], [2045, 9, 13], 3, 0.03], 0.11413], ['partial repair probe 1', [[2035, 1, 30], [2035, 7, 30], 6, 0.045], 2.25], ['partial repair probe 2', [[2015, 6, 30], [2016, 6, 30], 12, 0.03], 3.0], ['normal control 1', [[2018, 2, 28], [2018, 7, 27], 3, 0.045], 1.858696], ['normal control 2', [[2030, 12, 29], [2032, 6, 14], 12, 0.0725], 10.577869], ['normal control 3', [[2026, 3, 30], [2027, 6, 18], 12, 0.05], 6.092896], ['normal control 4', [[2038, 12, 13], [2039, 5, 4], 3, 0.05], 1.956522]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression short final stub 11.408841.40884Passed
regression short final stub 20.3349180.334918Passed
partial repair probe 11.1251.125Passed
partial repair probe 23.6253.625Passed
normal control 11.1739131.173913Passed
normal control 25.6465755.646575Passed
normal control 32.9281772.928177Passed
normal control 40.5645160.564516Passed

SHA-256 / 9489994585ae0f24281890250d3a61651d2469b6346d1faca86f68b62f051082

Verification & scope

A deterministic toy contract stated explicitly in the contract field; no claim of conformance to any published convention text. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:46:53.115691+00:00.

Case digest / 1f52637ebb6f504393923f414c26c9effd64a80c9ec39bf84ae0731123a41c1a