Debugging Day-Count Mismatches in Lease Interest Calculations
Diagnose an ASC 842 / IFRS 16 schedule whose opening liability and first interest figure disagree because present value used Actual/365 while accrual used 30/360, and thread one basis through both.
Problem Statement Link to this section
A lease schedule lands on the accountant's desk with a discrepancy that resists explanation: the opening liability ties out to the present-value working paper, and the payment vector is correct, yet the first period's interest is off by a few dollars and nothing downstream reconciles. The recursion is right, the rate is right, the balance is right — and the interest is still wrong. The cause is almost always a day-count mismatch: the present value discounted the payments using one convention (say Actual/365) while the interest accrual measured the first period on a different one (say 30/360). Because the two calculations disagree about how many days a period contains, the opening balance and the first interest figure are struck on incompatible clocks, and the schedule cannot be internally consistent. This page shows how to diagnose that mismatch, why the three common conventions produce different year fractions on the same two dates, and how to thread a single basis through both the discounting and the accrual so the schedule reconciles. It is one failure mode in the broader amortization precision troubleshooting cluster.
Standard Anchor Link to this section
Both frameworks require internal consistency between the measurement of the liability and the interest that unwinds it, even though neither prescribes a single day-count convention by name. ASC 842-20-30-1 measures the lease liability at the present value of the unpaid payments, and ASC 842-20-35-3 accrues interest on that carrying amount using the interest method; IFRS 16.26 and 16.36 set the same two-step structure. The standards leave the day-count basis to the contract and the entity's accounting policy — a real-estate lease may specify 30/360, an equipment lease Actual/365 — but they implicitly require that the same basis govern both steps, because the interest that produces a constant periodic rate on the remaining balance (IFRS 16.36) must be measured on the same time scale that placed the payments in the present-value sum. Discounting on Actual/365 and accruing on 30/360 breaks that constant-rate property at the first period and leaves a schedule an auditor cannot re-derive. The rate itself, whether the implicit rate or the lessee's incremental borrowing rate, is locked at commencement; the day-count basis must be locked alongside it.
Algorithm Specification: Three Conventions, One Pair of Dates Link to this section
Each convention computes a year fraction
30/360 treats every month as 30 days and every year as 360:
where
Actual/Actual counts real calendar days over the real length of the year, 365 or 366:
The first-period interest on an opening liability
Annotated Python Snippet: One Basis Through Both Steps Link to this section
The module below threads a single basis value through both the opening present value and the first-period accrual, then asserts that the first interest figure equals the opening balance times the periodic factor computed on that same basis. If a mismatch were reintroduced, the assertion would fail.
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
CENT = Decimal("0.01")
def year_fraction(start: date, end: date, basis: str) -> Decimal:
"""One year-fraction function, shared by PV and accrual."""
if basis == "30_360":
d1 = min(start.day, 30)
d2 = 30 if (end.day == 31 and d1 == 30) else end.day
days = (end.year - start.year) * 360 + (end.month - start.month) * 30 + (d2 - d1)
return Decimal(days) / Decimal("360")
if basis == "ACTUAL_ACTUAL":
denom = Decimal("366") if end.year % 4 == 0 else Decimal("365")
return Decimal((end - start).days) / denom
return Decimal((end - start).days) / Decimal("365") # ACTUAL_365
def opening_and_first_interest(payments, annual_rate, commencement, basis):
base = Decimal(1) + annual_rate
pv = Decimal("0")
for due, amount in payments: # PV on the shared basis
pv += amount / (base ** year_fraction(commencement, due, basis))
opening = pv.quantize(CENT, ROUND_HALF_UP)
first_due = payments[0][0] # accrual on the SAME basis
tau1 = year_fraction(commencement, first_due, basis)
interest1 = (opening * (base ** tau1 - Decimal(1))).quantize(CENT, ROUND_HALF_UP)
return opening, interest1, tau1
flows = [(date(2024, 2, 1), Decimal("15000.00")),
(date(2024, 3, 1), Decimal("15000.00")),
(date(2024, 4, 1), Decimal("15000.00"))]
opening, interest1, tau1 = opening_and_first_interest(
flows, Decimal("0.06"), date(2024, 1, 15), "ACTUAL_365")
# Both steps used one basis, so first interest reconciles to opening x period factor.
check = (opening * ((Decimal(1) + Decimal("0.06")) ** tau1 - Decimal(1))).quantize(CENT, ROUND_HALF_UP)
assert interest1 == check, "day-count mismatch: PV and accrual used different bases"
print(f"opening={opening} first interest={interest1} on tau={tau1}")
The assertion is the diagnostic made permanent: it recomputes the first interest from the opening balance and the same-basis factor, so any future refactor that lets the PV service and the schedule service default to different conventions breaks the build rather than the audit.
Correct vs. Incorrect: Three Conventions on the Same Dates Link to this section
Commencement 15 January 2024, first payment 1 February 2024, opening balance times annual rate equal to 15,000 for the period. Only the convention differs.
| Convention | Day-count numerator | Denominator | Year fraction | First interest |
|---|---|---|---|---|
| 30/360 | 16 adjusted days | 360 | 0.04444 | 666.67 |
| Actual/365 | 17 real days | 365 | 0.04658 | 698.63 |
| Actual/Actual | 17 real days | 366 (leap year) | 0.04645 | 696.72 |
| Mismatch (PV Actual/365, accrual 30/360) | conflicting | conflicting | inconsistent | fails re-derivation |
The three single-convention rows are each internally correct — the choice follows the contract. The final row is the defect: mixing Actual/365 discounting with 30/360 accrual produces an opening balance and a first interest figure that cannot both be right, and the schedule will not reconcile.
Gotcha: Two Services, Two Defaults Link to this section
The mismatch rarely comes from a single function using two conventions; it comes from two components each using its own default. A present-value microservice may default to Actual/365 while the schedule generator defaults to 30/360, and each is individually tested and individually correct. The bug only appears when they are wired together on a real lease.
pv = present_value(flows, rate, basis="ACTUAL_365") # PV service default
sched = build_schedule(pv, rate, basis="30_360") # schedule service default
# opening seeded on Actual/365, interest accrued on 30/360 -> irreconcilable
Guard against it by making basis a required, non-defaulted parameter that is set once from the lease record and passed explicitly into every step. Store the basis on the lease alongside the locked rate so both services read the same source of truth, and add the reconciliation assertion above to a shared test so a divergence fails in CI.
Frequently Asked Questions Link to this section
Which day-count convention should a lease use?
The one the contract specifies, or the entity's documented default where the contract is silent. Real-estate leases commonly use 30/360, which keeps every month's interest equal and simplifies reconciliation; equipment and many financing arrangements use an actual-days basis. There is no universally correct choice — the requirement is consistency, so whichever basis is selected must govern both the present value and the accrual and be locked to the lease alongside the discount rate.
How do I tell a day-count mismatch apart from a stub-period error?
Check what reconciles. In a day-count mismatch the opening liability and the first interest figure are struck on different conventions, so recomputing first interest as opening times the same-basis factor fails even though each component looks correct. A stub-period error uses one consistent basis but forgets that the first period is short, charging a full period of interest for a partial one. The mismatch shows two components disagreeing; the stub error shows one component ignoring elapsed days.
Does the day-count basis matter after the first period?
Yes, though it is most visible at commencement. When periods are regular full months the three conventions still differ slightly — 30/360 makes every month identical while an actual basis makes a 28-day February accrue less than a 31-day March — so a schedule that mixes bases drifts throughout, not only in period one. The first period is simply where a mid-month commencement makes the discrepancy largest and easiest to spot.
Related Link to this section
- Fixing floating-point drift in amortization schedules — the sibling failure mode where binary float, not the calendar, pushes the schedule off a zero close.
- Amortization precision troubleshooting — the parent guide covering all five failure modes that break a lease amortization engine.
- Liability amortization and schedule generation — the framework this schedule serves, where the day-count basis is chosen once and threaded through every stage.