Finance.DayCount behaviour (finance v1.7.0)

Copy Markdown View Source

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:

:basisconventionyear fraction
:actual_365Actual/365 Fixeddays / 365 (the default, matching spreadsheet XIRR)
:actual_360Actual/360days / 360
:actual_actualActual/Actual ISDAeach calendar year's days over its own length (365 or 366)
:thirty_36030/360 US (NASD)30-day months, day 31 pulled to 30
:thirty_e_36030E/360 Eurobondas 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_actual is 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's YEARFRAC(_, 1), which uses a different average-year-length method.
  • :thirty_360 is the basic US/NASD rule (Excel DAYS360 US): day 31 is pulled to 30, with no end-of-February adjustment (that EOM behaviour is a separate SIA variant, not shipped).
  • :thirty_e_360 leaves February alone, so Feb 28 → Mar 1 counts 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

basis()

@type basis() ::
  :actual_365
  | :actual_360
  | :actual_actual
  | :thirty_360
  | :thirty_e_360
  | module()

Callbacks

year_fraction(from, to, opts)

@callback year_fraction(from :: Date.t(), to :: Date.t(), opts :: keyword()) :: float()

The year fraction from from to to, where from <= to. opts carries convention-specific parameters; the built-in conventions ignore it.

Functions

bases()

@spec bases() :: [atom()]

The built-in :basis atoms.

year_fraction(date1, date2, basis, opts \\ [])

@spec year_fraction(Date.t(), Date.t(), basis(), keyword()) :: float()

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.0

30E/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