Skip to content

Add day-count conventions with a pluggable seam (1.7.0) - #29

Merged
tubedude merged 6 commits into
mainfrom
1.7.0
Jul 7, 2026
Merged

Add day-count conventions with a pluggable seam (1.7.0)#29
tubedude merged 6 commits into
mainfrom
1.7.0

Conversation

@tubedude

@tubedude tubedude commented Jul 7, 2026

Copy link
Copy Markdown
Owner

xirr/xnpv take a :basis option selecting how the year fraction between two dates
is measured. Five conventions ship built in: actual_365 (the default), actual_360,
actual_actual (ISDA), thirty_360 (US/NASD), and thirty_e_360 (Eurobond).

:basis also accepts any module implementing the new Finance.DayCount behaviour, so
a calendar-based convention this dependency-free library can't carry — Brazilian
Business/252, say — can live in the caller's app instead.

The default actual_365 reproduces the previous Date.diff/365, so existing results
are unchanged. Reference year fractions are cross-checked against Excel YEARFRAC.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn

tubedude and others added 2 commits July 6, 2026 09:39
xirr/xnpv take a :basis option selecting how the year fraction between two dates
is measured. Five conventions ship built in: actual_365 (the default), actual_360,
actual_actual (ISDA), thirty_360 (US/NASD), and thirty_e_360 (Eurobond).

:basis also accepts any module implementing the new Finance.DayCount behaviour, so
a calendar-based convention this dependency-free library can't carry — Brazilian
Business/252, say — can live in the caller's app instead.

The default actual_365 reproduces the previous Date.diff/365, so existing results
are unchanged. Reference year fractions are cross-checked against Excel YEARFRAC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
xnfv/2,3 (+ xnfv!) is the net future value of dated cash flows — the mirror of
xnpv, valuing them at the latest date rather than the earliest.

conventional?/1 reports whether a series changes sign exactly once, so it has a
single unambiguous IRR; a false flags that it may admit several valid rates
before the caller solves for one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
@coveralls

coveralls commented Jul 7, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same — 1.7.0 into main

tubedude and others added 3 commits July 7, 2026 08:08
- year_fraction is now /3 (from, to, opts): conventions that need context —
  Actual/Actual ICMA's coupon boundaries, 30E/360 ISDA's maturity carve-out —
  fit later without a breaking change. The built-in conventions ignore opts.
- Name the conventions precisely: actual_actual is Actual/Actual (ISDA), not
  Excel's basis-1 average-year method; thirty_360 is basic US/NASD with no
  end-of-February rule; thirty_e_360's Feb 28 -> Mar 1 counts 3 days (doctest'd).
- Pin the ACT/ACT per-year sum with a multi-year leap-straddling test.
- The business/252 recipe references ex_tempo (not the unrelated `tempo`
  package) and materializes a static ANBIMA business-day set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
- The CashFlow moduledoc and xnpv docs described Actual/365 as fixed; it is the
  default, selectable via :basis. xnpv/3's doc and the :basis schema doc now
  name :basis and xnfv.
- Under a 30/360 basis two distinct dates can share a period and merge; the
  normalize comment and a DayCount note now say so.
- Document xnfv's forward-compounding overflow, split the DayCount type into
  builtin | module, and pin conventional?([]) / [0, 0].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
Options:
- Four compiled schemas (dated rate, rate, dated value, value): each function
  validates only the options it uses, so an inapplicable option — :basis on
  periodic irr, :guess on xnpv or a Bonds metric — raises instead of being
  silently ignored. npv validates options before rejecting empty data.
- :precision is 0..15 in the Returns and amortization schemas too, and
  Finance.option includes {:basis, _}.

Behavior:
- xnfv returns {:error, :undefined} when forward compounding overflows.
- The Returns cash-flow metrics coerce amounts via Shared.to_amount, so
  Decimal and Money inputs work there like they do in CashFlow.

Tests: Decimal/Money value-transparency property; cross-basis contracts
(irr == xirr, xnfv/xnpv ratio, batch with basis + Brent together); a 30/360
Feb-29 no-EOM pin; fractional bond years.

Docs: restore the 1.6.1 CHANGELOG heading; README pins ~> 1.7 and gains
DayCount, xnfv/conventional?, and :undefined in the error table; ExDoc gets
source_ref and module groups; the first xirr doctest shows a simple 20% rate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
Cases drawn from other XIRR/financial libraries' issue trackers, pinned so a
solver or day-count change can't silently regress them:

- XIRR: java-xirr #5b (a yield below -83%) and #20 (a series with no real IRR,
  which returns a clean :did_not_converge where java-xirr spun to its cap).
- DayCount: the Actual/Actual (ISDA) 2006 gold values, and a 30/360 case pinning
  that we follow Excel DAYS360 rather than the NASD end-of-February rule.
- Bonds: a discounted zero-coupon yield that QuantLib #256 underestimated.
- TVM.rate: the 30-year low-rate loan from POI bug 65988, Microsoft's 48-month
  example, and a deep-negative guess-selected root.
- IRR: the 46-flow near-zero-rate series POI bug 64137 could not converge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
@tubedude
tubedude marked this pull request as draft July 7, 2026 13:48
@tubedude
tubedude marked this pull request as ready for review July 7, 2026 13:48
@tubedude tubedude self-assigned this Jul 7, 2026
@tubedude
tubedude merged commit b1daf2b into main Jul 7, 2026
5 checks passed
@tubedude
tubedude deleted the 1.7.0 branch July 7, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants