Day-count conventions — the fraction of a year between two dates.
Dated cash-flow functions (Finance.CashFlow.xirr/2, xnpv/2) discount each
flow by (1 + rate)^t, where t is the year fraction from the earliest date to
the flow's date. Which fraction to use depends on the market convention the
instrument is quoted under, selected with the :basis option.
Five conventions ship built in, named by atom:
:basis | convention | year fraction |
|---|---|---|
:actual_365 | Actual/365 Fixed | days / 365 (the default, matching spreadsheet XIRR) |
:actual_360 | Actual/360 | days / 360 |
:actual_actual | Actual/Actual ISDA | each calendar year's days over its own length (365 or 366) |
:thirty_360 | 30/360 US (NASD) | 30-day months, day 31 pulled to 30 |
:thirty_e_360 | 30E/360 Eurobond | as 30/360, day 31 pulled to 30 on both ends |
Both "Actual/Actual" and "30/360" name more than one method, so to be precise about which ones ship:
:actual_actualis Actual/Actual (ISDA) — the sensible choice for irregular cash flows. Each calendar year the span touches contributes its own days over its own length, so leap years are weighted exactly. It is not Excel'sYEARFRAC(_, 1), which uses a different average-year-length method.:thirty_360is the basic US/NASD rule (ExcelDAYS360US): day 31 is pulled to 30, with no end-of-February adjustment (that EOM behaviour is a separate SIA variant, not shipped).:thirty_e_360leaves February alone, soFeb 28 → Mar 1counts three days. That is a known property of the convention, not a bug.
Custom conventions
:basis also accepts any module implementing this behaviour — a single
year_fraction/3 callback. The third argument carries convention-specific
parameters (coupon-period boundaries, frequency, a maturity flag) that methods
like Actual/Actual ICMA or 30E/360 ISDA need; the built-in static conventions
ignore it.
A convention that needs a holiday calendar this dependency-free library can't
carry — Brazil's Business/252, where the fraction is business_days(from, to) / 252 against the ANBIMA calendar — lives in your app. Prefer materializing the
market's published holidays into a static business-day set and counting against
it (a library like Tempo can build that set
from an .ics file and refresh it when new dates are published) over computing
it at runtime:
defmodule MyApp.Business252 do
@behaviour Finance.DayCount
@impl true
def year_fraction(from, to, _opts) do
MyApp.Holidays.business_days_between(from, to) / 252
end
end
Finance.CashFlow.xirr(flows, basis: MyApp.Business252)
Summary
Callbacks
The year fraction from from to to, where from <= to. opts carries
convention-specific parameters; the built-in conventions ignore it.
Functions
The built-in :basis atoms.
The year fraction from date1 to date2 under basis — a built-in atom or a
module implementing Finance.DayCount. Assumes date1 <= date2. opts is
passed through to a custom module and ignored by the built-in conventions.
Types
@type basis() :: :actual_365 | :actual_360 | :actual_actual | :thirty_360 | :thirty_e_360 | module()
Callbacks
Functions
@spec bases() :: [atom()]
The built-in :basis atoms.
The year fraction from date1 to date2 under basis — a built-in atom or a
module implementing Finance.DayCount. Assumes date1 <= date2. opts is
passed through to a custom module and ignored by the built-in conventions.
iex> Finance.DayCount.year_fraction(~D[2019-01-01], ~D[2020-01-01], :actual_365)
1.0
iex> Finance.DayCount.year_fraction(~D[2019-01-01], ~D[2020-01-01], :actual_360)
1.0138888888888888
iex> Finance.DayCount.year_fraction(~D[2019-01-01], ~D[2020-01-01], :thirty_360)
1.030E/360 leaves February untouched, so Feb 28 → Mar 1 is three days, not one:
iex> Finance.DayCount.year_fraction(~D[2019-02-28], ~D[2019-03-01], :thirty_e_360)
0.008333333333333333