02 · Interest

Accrue CAD interest at posting boundaries

Accrue daily interest on a CAD balance and post it at month ends, using Actual/365 and banker's rounding.

Python · Decimalmedium

What the hardened grader checks

Solutions

Reference

reference

Accrues day by day in high-precision Decimal and rounds half-even only at each posting.

Naive: accepted (4/4)Hardened: accepted (33/33)

Alternative

alt_valid

Closed-form exact rational segments and manual integer half-even rounding instead of daywise Decimal accrual.

Naive: accepted (4/4)Hardened: accepted (33/33)

wrong_actual_actual

Uses Actual/Actual with a 366-day denominator in leap years instead of Actual/365 Fixed.

Naive: accepted (4/4)Hardened: rejected (29/33)

wrong_float

Binary floating point loses precision on ties and large principals.

Naive: accepted (4/4)Hardened: rejected (28/33)

wrong_half_up

Rounds ties away from zero instead of to even.

Naive: accepted (4/4)Hardened: rejected (28/33)

Held-out solutions

Tests × solutions

Each row groups a test’s parameterized cases; any failing case marks the row as Fail. * marks a wrong solution’s declared must_fail test.

Hardened · 33 recorded cases · 7 test rows
TestStarterReferenceAlternativeactual actualfloathalf up
Half cent ties bankersFailPassPassPassFail *Fail *
Invalid money raisesPassPassPassPassPassPass
Invalid dates and method raisePassPassPassPassPassPass
Large principal exactFailPassPassFail *Pass *Pass
Small balance daily accruesPassPassPassPassPassPass
Leap and partialFailPassPassFail *PassPass
Accrual propertiesFailPassPassFail *PassPass
Naive grader
Naive · 4 recorded cases · 3 test rows
TestStarterReferenceAlternativeactual actualfloathalf up
ExamplesPassPassPassPassPassPass
Ticket worked examplePassPassPassPassPassPass
Empty period and reversed datesPassPassPassPassPassPass

The ticket

Ticket: accrue interest across partial calendar months

The synthetic CAD account needs correct daily accrual and explicit posting boundaries. Keep interest.accrual.accrue(principal: Decimal, annual_rate: Decimal, start: date, end: date, method: Literal["simple_monthly","daily_compound"]) -> list[Posting] and Posting(posting_date: date, interest: Decimal, balance: Decimal) (frozen dataclass).

CAD has 2 decimal places. Principal is a nonnegative finite Decimal in whole cents; rate is a nonnegative finite Decimal (a fraction, e.g. 0.05 means 5%). Invalid values/method or end before start raise ValueError. These are dates, not timestamps; any datetime instead of date is invalid.

  • Day count is Actual/365 Fixed, including leap years. Accrue for every date in [start,end).
  • Post on each month end within that period, and also on end - 1 day if it is not a month end. Return chronological postings. start == end returns [].
  • simple_monthly: interest at a posting equals current posted balance × rate × segment days / 365. Add each posting to the balance; thus it compounds at posting boundaries.
  • daily_compound: each date, accrued += (posted balance + accrued) × rate / 365 with full intermediate precision. At posting, round accrued, add to balance, reset accrued to zero; discard the unrounded residual.
  • Round only the posting to 0.01 using ROUND_HALF_EVEN. Intermediate arithmetic must use precision at least 34 or exact arithmetic. Mathematically exact half-cent ties must round half-even, without intermediate rounding changing which side of the tie is selected. Both money fields must be Decimal with exponent -2, even when zero. Return the resulting balance after each posting.

Worked example: principal 1000.00, rate 0.0365 (so 0.0001 per day), start 2026-01-01, end 2026-03-01. Both methods post on 2026-01-31 (31 days) and 2026-02-28 (28 days). January: 1000.00 x 0.0365 x 31 / 365 = 3.10, balance 1003.10. February: 1003.10 x 0.0365 x 28 / 365 = 2.8087..., posted as 2.81, balance 1005.91. (With daily_compound the unrounded January interest is 3.1048...; both methods round to the same cents here.)

Only the declared interface is graded; internal representation and algorithms are unrestricted. Run from starter: python -m pytest tests -q. All examples and accounts are synthetic.

Source