FinancePy Act/365L can fail when an optional date is omitted
DayCount.year_frac() documents an ordinary two-date mode where dt3 is absent. In FinancePy 1.0.1, an annual Act/365L interval that reaches February 29 can dereference that missing object instead of returning a fraction.
A one-minute explanation
Synthetic English narration using macOS Samantha. Original motion cards, no music or third-party imagery. Download the script, claim ledger, renderer and QA record.
Released-wheel reproduction
The documented ordinary mode sets dt3 to None. This synthetic call uses 1 December 2023 as the start, 1 March 2024 as the end, and annual Act/365L:
DayCount(DayCountTypes.ACT_365L).year_frac(
Date(1, 12, 2023),
Date(1, 3, 2024),
freq_type=FrequencyTypes.ANNUAL,
)
FinancePy 1.0.1 raises AttributeError: 'NoneType' object has no attribute 'excel_dt'. Supplying dt3=dt2 returns (0.24863387978142076, 91.0, 366).
The independent result
Python’s standard-library calendar gives 91 actual days. The cited annual Act/365L definition uses 366 when the relevant period contains February 29. The expected fraction is therefore:
actual days 91
denominator 366
year fraction 91 / 366 = 0.24863387978142076
OpenGamma Strata also describes annual Act/365L in terms of the period end or next coupon date. For an ordinary complete period, the calculation end is that period end. For an accrued fraction, the separately supplied coupon-period end remains distinct.
Why the exception occurs
The implementation calculates a year from dt2 when dt3 is absent, but it does not assign a date to dt3. If a leap day is selected, the annual branch later evaluates:
if feb29 > dt1 and feb29 <= dt3:
den = 366
The second comparison reaches Date.__le__ with dt3=None. Intervals that do not reach the relevant leap day can conceal the problem through boolean short-circuiting.
Submitted correction and falsification
FinancePy pull request #259 assigns dt2 to dt3 only when the optional reference-period end was not supplied. A caller-provided coupon end is left unchanged.
- The focused day-count file reports 11 passes.
- The complete local unit suite reports 959 passes, with four pre-existing LSMC
RankWarningmessages. - Restoring the former
Nonehandling reproduces the exception. - A separate control confirms that an explicitly later coupon end still selects the accrued-fraction denominator.
- Both official upstream workflows report success on commit
a93243c4982dc77432d3405d478c34998e0f04a6.
At publication time the pull request remains open and unmerged. Passing checks establish the status of the submitted change; they do not imply maintainer acceptance.
Evidence and limits
The public PyPI wheel has SHA-256 110e784122f485ae207239f44af473334f418d81bcbb958f5eda4e76956feca5. The audited day_count.py inside it has SHA-256 da564a6105eecde08b0f302b715a76fd5798e46b565409c2b5fb7de897ed70a7. The same control-flow failure remains at upstream base commit 2b9227fea9d832c4033421d6cd53a54316414fca.
A bounded search of FinancePy issues and pull requests for four direct formulations found no prior report. That is not a worldwide priority guarantee.
All dates are synthetic. The report establishes a released library failure. It does not establish institutional use, downstream exposure, customer loss, security impact, or production deployment.
- Complete public reproducer
- Submitted correction: FinancePy PR #259
- FinancePy 1.0.1 on PyPI
- OpenGamma Strata day-count documentation
- Download the reproducible evidence ZIP — SHA-256
f219e467e775e5b22d06abfc6b7f5fe68116a723d2ff93952d1bd0cda2658e07
The public reproducer and report are supplied under GPL-3.0-or-later. Source excerpts retain their upstream licensing.
