Financial calculations for cash-flow analysis: internal rate of return, net present value, modified IRR, and the usual time-value-of-money and depreciation helpers.
The functions fall into four families, grouped by the question they answer:
- Rate of return — when you have a stream of cash flows and want to know
the rate they imply.
xirr/2andirr/1find that rate;mirr/3answers a variation of the same question.xirr/2works with{date, amount}flows at arbitrary dates and discounts on an Actual/365 basis, matching the spreadsheetXIRR;irr/1is its counterpart for amounts spread over equally spaced periods0, 1, 2, …. - Present and future value — when you know a rate and want to value a
stream of flows against it.
xnpv/2handles dated flows,npv/2handles periodic ones. - Time value of money —
fv/5,pv/5,pmt/5,nper/5, andrate/6each take a standard annuity and solve it for one unknown, the way a financial calculator does. - Depreciation —
sln/3,syd/4,ddb/5, anddb/5write an asset down from its cost to its salvage value over its useful life, either evenly or on an accelerated schedule.
The centerpiece is XIRR, which finds the rate r that brings the net present
value of a set of dated flows to zero:
Σ cf_i / (1 + r)^t_i = 0where t_i is the number of years between a flow and the earliest one in the
series. There is no closed form for this, so the solver looks for the root
numerically: it runs Newton-Raphson, using the analytic derivative of the NPV,
and falls back to a bracketing bisection when a Newton step wanders outside the
valid domain or fails to converge. Throughout, it follows the conventions a
spreadsheet XIRR uses — Actual/365 day counting, a 0.1 starting guess, and
a cap of 100 iterations.
Example
iex> Finance.xirr([{~D[2019-01-01], -1000}, {~D[2020-01-01], 1100}])
{:ok, 0.1}You can pass dated flows as {date, amount} pairs or as two parallel lists of
dates and amounts, whichever reads better at the call site. Dates may be Date
structs or Erlang-style {year, month, day} tuples. Amounts may be any number
— integer minor units such as cents, or floats — or a Decimal when you have
that optional dependency installed. Whatever you feed in, the result comes back
as a float.
Options
The rate-finding and value functions take an optional keyword list to tune the
solver and the rounding. Options are validated with nimble_options: passing
an unknown key or a bad value raises, since that is a mistake in your code,
whereas problems with the data itself come back as {:error, reason} for you
to handle.
:guess(float/0) - initial rate for the Newton-Raphson solver The default value is0.1.:tolerance(float/0) - convergence threshold on the net present value The default value is1.0e-9.:max_iterations(pos_integer/0) - cap on solver iterations before giving up The default value is100.:precision(non_neg_integer/0) - decimal places the result is rounded to The default value is6.
Summary
Types
A cash-flow amount. This can be any number, or a Decimal when you have that optional dependency installed.
A cash flow on a given date. Money coming in is positive, money going out is negative.
An annual rate of return expressed as a fraction, so 0.1 means 10%.
Functions
Computes fixed-declining-balance depreciation for a single period.
Same as db/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes double-declining-balance depreciation for a single period.
Same as ddb/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the future value of an investment: what it grows to after nper
periods, starting from a present value of pv, with a fixed payment of pmt
each period, all compounding at rate.
Same as fv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes the IRR — the internal rate of return for a list of amounts that
occur at equally spaced periods 0, 1, 2, …. Think of it as xirr/2 for the
common case where your flows land at regular intervals and you don't need to
track exact dates.
Same as irr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes the MIRR — the modified internal rate of return for periodic
amounts. It refines the idea behind IRR by letting you set two separate
rates: positive flows are assumed to be reinvested at reinvest_rate, and
negative flows are assumed to be financed at finance_rate.
Same as mirr/3, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Works out how many periods it takes for payments of pmt to pay off a present
value pv (reaching future value fv) at rate.
Same as nper/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes the periodic NPV — the net present value of amounts occurring at
equally spaced periods 0, 1, 2, …, discounted at rate.
Same as npv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the level payment per period needed to pay off a present value pv
(and arrive at a future value fv) over nper periods at rate.
Same as pmt/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the present value of an investment: what a future stream is worth
today. The stream is pmt paid each period for nper periods plus a lump sum
fv at the end, all discounted back at rate.
Same as pv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the interest rate per period of an annuity described by nper
payments of pmt, a present value pv, and a future value fv. nper has to
be a whole number of periods.
Same as rate/6, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes straight-line depreciation: the equal amount an asset is written down
by each period as it declines from its cost to its salvage value over
life periods.
Same as sln/3, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes sum-of-years'-digits depreciation for a single period (counting from
1).
Same as syd/4, but hands back the value on its own and raises ArgumentError if the calculation fails.
Finds the XIRR of a list of {date, amount} cash flows — the annual rate of
return that the flows imply, given when each one lands.
Finds the XIRR, accepting either {date, amount} pairs together with options,
or two parallel lists — one of dates, one of amounts.
Finds the XIRR from two parallel lists — dates and amounts — while also taking options for the solver.
Same as xirr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Same as xirr/2, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes the XNPV — the net present value of a set of dated cash flows,
discounted back at rate.
Same as xnpv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.
Types
A cash-flow amount. This can be any number, or a Decimal when you have that optional dependency installed.
A cash flow on a given date. Money coming in is positive, money going out is negative.
A date, given either as a Date struct or as an Erlang-style {year, month, day} tuple.
@type error() ::
:mismatched_lengths
| :insufficient_data
| :single_signed_flow
| :invalid_date
| :did_not_converge
| :undefined
@type option() :: {:guess, number()} | {:tolerance, number()} | {:max_iterations, pos_integer()} | {:precision, non_neg_integer()}
@type rate() :: float()
An annual rate of return expressed as a fraction, so 0.1 means 10%.
Functions
Computes fixed-declining-balance depreciation for a single period.
Like ddb/5, this applies a constant rate to the declining book value, but the
rate is derived directly from cost, salvage, and life (and rounded to
three decimal places, matching what spreadsheets do). Use month to say how
many months the asset was in service during its first year; it defaults to a
full 12, and a shorter first year spills the remaining depreciation into an
extra final period.
iex> Finance.db(10_000, 1_000, 5, 1)
{:ok, 3690.0}
iex> Finance.db(10_000, 1_000, 5, 2)
{:ok, 2328.39}
Same as db/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes double-declining-balance depreciation for a single period.
This is another accelerated method: each period it takes a fixed fraction of
the remaining book value, so the write-down shrinks as the asset ages. The
factor sets how aggressive that fraction is, defaulting to 2 for the usual
double-declining rate. The depreciation is capped so it never drives the book
value below salvage.
iex> Finance.ddb(10_000, 1_000, 5, 1)
{:ok, 4000.0}
iex> Finance.ddb(10_000, 1_000, 5, 2)
{:ok, 2400.0}
Same as ddb/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the future value of an investment: what it grows to after nper
periods, starting from a present value of pv, with a fixed payment of pmt
each period, all compounding at rate.
Use this to answer "if I put this much in now and add this much every period,
what will I have at the end?". As with the rest of the time-value-of-money
functions, type chooses when each payment happens — 0 for the end of the
period (an ordinary annuity) or 1 for the beginning (an annuity due) — and
the sign convention follows spreadsheets, so money you receive is positive and
money you pay out is negative.
iex> {:ok, value} = Finance.fv(0.05, 10, -100, -1000)
iex> Float.round(value, 2)
2886.68
Same as fv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes the IRR — the internal rate of return for a list of amounts that
occur at equally spaced periods 0, 1, 2, …. Think of it as xirr/2 for the
common case where your flows land at regular intervals and you don't need to
track exact dates.
What comes back is the rate per period. As with xirr/2, the series has to
contain at least one positive amount and one negative one.
iex> Finance.irr([-1000, 1100])
{:ok, 0.1}
iex> Finance.irr([-1000, 500, 500, 300])
{:ok, 0.156579}
Same as irr/1, and additionally takes the same options as xirr/2.
Same as irr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes the MIRR — the modified internal rate of return for periodic
amounts. It refines the idea behind IRR by letting you set two separate
rates: positive flows are assumed to be reinvested at reinvest_rate, and
negative flows are assumed to be financed at finance_rate.
Because those assumptions are spelled out, MIRR has a closed form and a single
answer, which sidesteps the multiple-root and convergence trouble that IRR can
run into. As with irr/1, the series has to contain at least one positive
amount and one negative one.
iex> Finance.mirr([-120_000, 39_000, 30_000, 21_000, 37_000, 46_000], 0.10, 0.12)
{:ok, 0.126094}
Same as mirr/3, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Works out how many periods it takes for payments of pmt to pay off a present
value pv (reaching future value fv) at rate.
This is the "how long until it's paid off?" question. When the numbers don't
describe a situation that ever resolves, it returns {:error, :undefined}.
iex> {:ok, periods} = Finance.nper(0.05, -100, 1000)
iex> Float.round(periods, 2)
14.21
Same as nper/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes the periodic NPV — the net present value of amounts occurring at
equally spaced periods 0, 1, 2, …, discounted at rate.
Σ amount_i / (1 + rate)^i (i starting at 0)Convention
The first amount sits at period 0 and so is left undiscounted. That is what
lets npv/2 and irr/1 line up: npv(irr(a), a) comes out to roughly
zero. It also means the result differs from a spreadsheet NPV, which
places the first amount at period 1. If you want to match a spreadsheet,
discount the first amount yourself or prepend a leading 0 to the list.
iex> Finance.npv(0.1, [-1000, 1100])
{:ok, 0.0}
iex> Finance.npv(0.1, [-1000, 600, 600])
{:ok, 41.322314}
Same as npv/2, and additionally takes a :precision option. See npv/2.
Same as npv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the level payment per period needed to pay off a present value pv
(and arrive at a future value fv) over nper periods at rate.
This is the loan-payment question: given an amount borrowed today, what fixed
installment clears it over the term? The same type and sign conventions
apply, so a loan you take out is a positive pv and the payment comes back
negative.
iex> {:ok, payment} = Finance.pmt(0.10, 10, 1000)
iex> Float.round(payment, 2)
-162.75
Same as pmt/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
Works out the present value of an investment: what a future stream is worth
today. The stream is pmt paid each period for nper periods plus a lump sum
fv at the end, all discounted back at rate.
It answers the mirror image of fv/5's question — "how much would I need to
put in now to fund these future payments?". The same type and sign
conventions apply.
iex> {:ok, value} = Finance.pv(0.05, 10, -100, -1000)
iex> Float.round(value, 2)
1386.09
Same as pv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.
@spec rate(number(), number(), number(), number(), 0 | 1, [option()]) :: {:ok, rate()} | {:error, error()}
Works out the interest rate per period of an annuity described by nper
payments of pmt, a present value pv, and a future value fv. nper has to
be a whole number of periods.
There is no closed form for the rate, so this reuses the same numerical solver
as irr/1 and takes the same options as xirr/2. If it can't pin down a rate,
it returns {:error, :did_not_converge}.
iex> Finance.rate(10, -100, 1000)
{:ok, 0.0}
Same as rate/6, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes straight-line depreciation: the equal amount an asset is written down
by each period as it declines from its cost to its salvage value over
life periods.
This is the plainest of the depreciation methods — it spreads the loss in value evenly, so every period sees the same write-down.
iex> Finance.sln(10_000, 1_000, 5)
{:ok, 1800.0}
Same as sln/3, but hands back the value on its own and raises ArgumentError if the calculation fails.
Computes sum-of-years'-digits depreciation for a single period (counting from
1).
This is an accelerated method: it charges more depreciation in the early
periods and less later on, which suits assets that lose most of their value up
front. It returns the write-down for the one period you ask about rather than
the whole schedule.
iex> Finance.syd(10_000, 1_000, 5, 1)
{:ok, 3000.0}
iex> Finance.syd(10_000, 1_000, 5, 5)
{:ok, 600.0}
Same as syd/4, but hands back the value on its own and raises ArgumentError if the calculation fails.
Finds the XIRR of a list of {date, amount} cash flows — the annual rate of
return that the flows imply, given when each one lands.
Reach for this when your cash flows happen on irregular dates rather than at
neat intervals. See xirr/2 if you want to pass options or use the two-list
form.
iex> 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}
@spec xirr([cash_flow()], [option()]) :: {:ok, rate()} | {:error, error()}
@spec xirr([date()], [amount()]) :: {:ok, rate()} | {:error, error()}
Finds the XIRR, accepting either {date, amount} pairs together with options,
or two parallel lists — one of dates, one of amounts.
The result is {:ok, rate} on success or {:error, reason} when the data
can't yield a rate. Flows that fall on the same date are added together first.
The series has to include at least one positive amount and one negative one,
because without money flowing both in and out there is no return to solve for.
iex> Finance.xirr([{~D[2019-01-01], -1000}, {~D[2020-01-01], 1100}], guess: 0.5)
{:ok, 0.1}
iex> Finance.xirr([~D[2019-01-01], ~D[2020-01-01]], [-1000, 1100])
{:ok, 0.1}
Finds the XIRR from two parallel lists — dates and amounts — while also taking options for the solver.
iex> Finance.xirr([~D[2019-01-01], ~D[2020-01-01]], [-1000, 1100], precision: 2)
{:ok, 0.1}
Same as xirr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Same as xirr/2, but hands back the rate on its own and raises ArgumentError if the calculation fails.
Computes the XNPV — the net present value of a set of dated cash flows,
discounted back at rate.
Σ cf_i / (1 + rate)^t_iEach time t_i is measured in years from the earliest flow on an Actual/365
basis, the same day-count convention xirr/2 uses. That shared convention is
what ties the two together: xnpv(r, flows) comes out to roughly zero when
r is xirr(flows), so this is a handy way to check an XIRR result. And
because a net present value is defined for any series, the flows here don't
have to change sign the way xirr/2 requires.
The result is {:ok, value} or {:error, reason}. Flows on the same date are
added together first, and you can pass the same :precision option as
xirr/2 (it defaults to 6).
iex> Finance.xnpv(0.1, [{~D[2019-01-01], -1000}, {~D[2020-01-01], 1000}])
{:ok, -90.909091}
iex> Finance.xnpv(0.1, [{~D[2019-01-01], -1000}, {~D[2020-01-01], 1100}])
{:ok, 0.0}
Same as xnpv/2, and additionally takes a :precision option. See xnpv/2.
Same as xnpv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.