Fix 30/360 US, 30/365 and Act/365 Canadian day counters diverging from QuantLib - #324
Merged
amaggiulli merged 1 commit intoAug 4, 2026
Conversation
Three day counters drifted from their QuantLib counterparts: - Thirty360 (US) applied the last-of-February adjustments after the 31 -> 30 checks instead of before, so a period starting on the last day of February and ending on a 31st gained one day. - Thirty365 was missing the ISO 20022 31 -> 30 adjustments, which let a one-day interval accrue zero days. - Actual365Fixed (Canadian) lacked the frequency != 0 guard, so a reference period longer than a year raised DivideByZeroException rather than the intended domain error. Adds table-driven tests for the US and NASD conventions, extends the 30/365 test with 31st endpoints, and covers the Canadian guard.
There was a problem hiding this comment.
Pull request overview
This PR aligns QLNet’s 30/360 (USA), 30/365, and Actual/365 (Fixed) Canadian day counters with QuantLib’s behavior, addressing previously observed divergences and adding targeted regression tests to lock in the corrected results.
Changes:
- Fixes 30/360 (USA) end-of-February handling by applying the last-of-February adjustments before the
31 -> 30checks (order-dependent). - Implements the ISO 20022
31 -> 30endpoint adjustments in 30/365 day counting. - Adds a guard in Actual/365 (Fixed) Canadian to reject reference periods longer than one year (avoids zero-frequency division) and extends the test suite accordingly.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/QLNet.Tests/T_DayCounters.cs | Adds/extends regression tests for 30/365 31st endpoints, 30/360 (USA), 30/360 (NASD), and the Canadian long reference-period guard. |
| src/QLNet/Time/DayCounters/Thirty365.cs | Adds ISO 20022 31 -> 30 adjustments to match QuantLib day-count behavior for 30/365. |
| src/QLNet/Time/DayCounters/Thirty360.cs | Reorders and refines 30/360 (USA) adjustments so end-of-February logic is applied in the correct order. |
| src/QLNet/Time/DayCounters/Actual365Fixed.cs | Adds a frequency != 0 requirement for the Canadian convention to prevent divide-by-zero for long reference periods. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Owner
|
👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three day counters return different values from the QuantLib functions they port. Numbers below are from QuantLib 1.43 against QLNet
develop.Thirty360(USA).dayCount(2007-02-28, 2007-03-31)Thirty360(USA).dayCount(2008-02-29, 2008-08-31)Thirty365().dayCount(2007-01-31, 2007-02-01)Thirty365().dayCount(2007-01-01, 2007-01-31)Actual365Fixed(Canadian), 2-year reference periodDivideByZeroExceptionUsImplruns the last-of-February adjustments after the31 -> 30checks, so D1 is still 28/29 whendd2 == 31 && dd1 >= 30is tested and D2 never collapses to 30.thirty360.cppUS_Impl::dayCountputs the February block first, under the comment// NOTE: the order of checks is important, and the class docstring here already describes that behaviour.Thirty365is missing the two ISO 2002231 -> 30adjustments thatthirty365.cppapplies, which is why a one-day interval can accrue zero.CA_Implis missingQL_REQUIRE(frequency != 0, ...)fromactual365fixed.cpp.I compared every counter in
Time/DayCountersagainst QuantLib 1.43 over 48,205 date pairs each (month ends, 31sts, Feb 28/29, leap and non-leap years). Only these three diverged: Bond Basis/ISMA, European/Eurobond, Italian, ISDA/German with and without a termination date, NASD, Act/360, Act/364, Act/366, Act/365.25, Act/365F Standard and NoLeap, Act/Act ISDA/AFB/ISMA, Simple, 1/1 and Business/252 all matched already, and all 27 match now.Adds
testThirty360_USAandtestThirty360_NASD(NASD was already correct, just untested), extendstestThirty365with 31st endpoints, and adds the Canadian guard case. Expected values are QuantLib output. Suite: 533 passed, 0 failed.