Finance
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:
| Reason | Meaning |
|---|---|
:mismatched_lengths | date and amount lists differ in length |
:insufficient_data | fewer than two distinct-date flows |
:single_signed_flow | all amounts have the same sign |
:invalid_date | a date could not be parsed |
:did_not_converge | no 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.