Finance

CI

An Elixir library for cash-flow analysis. It covers internal rate of return (xirr/irr), net present value (xnpv/npv), and modified IRR (mirr), along with the usual time-value-of-money and depreciation helpers. Options are validated with nimble_options, and amounts may be Decimal values when you have that optional dependency installed.

The rate and value functions come in two forms. The dated ones (xirr, xnpv) work with flows that land on arbitrary dates, discounting on an Actual/365 basis to match spreadsheet XIRR/XNPV. The periodic ones (irr, npv, mirr) take a plain list of amounts spread over equally spaced periods, for when the exact dates don't matter.

Installation

Add finance to your dependencies in mix.exs:

def deps do
[{:finance, "~> 1.0"}]
end

If you also want to pass Decimal amounts, add {:decimal, "~> 3.0"} alongside it.

Usage

Pass a list of {date, amount} cash flows. Money coming in is positive and money going out is negative, and the series needs at least one of each — without flows in both directions there is no rate to solve for.

Finance.xirr([
{~D[2015-06-01], 1_000_000},
{~D[2015-10-01], -2_200_000},
{~D[2015-11-01], -800_000}
])
#=> {:ok, 21.118359}

Dates can also be {year, month, day} tuples, and if it reads better you can supply two parallel lists instead of pairs:

Finance.xirr([{2019, 1, 1}, {2020, 1, 1}], [-1000, 1100])
#=> {:ok, 0.1}

If you would rather work with the rate directly than unwrap an :ok tuple, xirr!/1 and xirr!/2 return it on its own and raise on error.

Periodic functions

For flows at equally spaced periods 0, 1, 2, …, pass a plain list of amounts:

Finance.irr([-1000, 500, 500, 300]) #=> {:ok, 0.156579}
Finance.npv(0.1, [-1000, 600, 600]) #=> {:ok, 41.322314}
Finance.mirr([-120_000, 39_000, 30_000, 21_000, 37_000, 46_000], 0.10, 0.12)
#=> {:ok, 0.126094}

One thing to watch: npv/2 places the first amount at period 0, which is what makes npv(irr(a), a) ≈ 0 hold. A spreadsheet NPV instead places the first amount at period 1, so the two won't agree unless you account for that.

Amounts and Decimal

Amounts may be any number — integer minor units such as cents, or floats. If your app already depends on Decimal, you can pass Decimal values straight through, with no conversion on your side:

Finance.xirr([{~D[2019-01-01], Decimal.new("-1000")}, {~D[2020-01-01], Decimal.new("1100")}])
#=> {:ok, 0.1}

Decimal is an optional dependency, so apps that don't use it pull in nothing extra. Either way the result comes back as a float: XIRR's math is inherently irrational, so accepting Decimal is about convenience at the call site, not added precision in the answer.

Errors

When the data can't produce a result, xirr/1 and xirr/2 return {:error, reason}, where reason is one of:

ReasonMeaning
:mismatched_lengthsdate and amount lists differ in length
:insufficient_datafewer than two distinct-date flows
:single_signed_flowall amounts have the same sign
:invalid_datea date could not be parsed
:did_not_convergeno rate found within the iteration limit

Development

mix deps.get
mix test
mix format
mix credo --strict
mix dialyzer

See CHANGELOG.md for the 1.0 rewrite notes.