Finance (finance v1.1.0)

Copy Markdown View Source

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/2 and irr/1 find that rate; mirr/3 answers a variation of the same question. xirr/2 works with {date, amount} flows at arbitrary dates and discounts on an Actual/365 basis, matching the spreadsheet XIRR; irr/1 is its counterpart for amounts spread over equally spaced periods 0, 1, 2, ….
  • Present and future value — when you know a rate and want to value a stream of flows against it. xnpv/2 handles dated flows, npv/2 handles periodic ones.
  • Time value of moneyfv/5, pv/5, pmt/5, nper/5, and rate/6 each take a standard annuity and solve it for one unknown, the way a financial calculator does.
  • Depreciationsln/3, syd/4, ddb/5, and db/5 write an asset down from its cost to its salvage value over its useful life, either evenly or on an accelerated schedule.
  • Volatilityvolatility/2 annualises the standard deviation of a price series' returns.

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

where 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 is 0.1.

  • :tolerance (float/0) - convergence threshold on the net present value The default value is 1.0e-9.

  • :max_iterations (pos_integer/0) - cap on solver iterations before giving up The default value is 100.

  • :precision (non_neg_integer/0) - decimal places the result is rounded to The default value is 6.

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.

A date, given either as a Date struct or as an Erlang-style {year, month, day} tuple.

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

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

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.

Annualised volatility of a price series — the standard deviation of its period-over-period returns, scaled up to a yearly figure.

Same as volatility/2, but hands back the value on its own and raises ArgumentError if it can't be computed.

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

Types

amount()

@type amount() :: number() | Decimal.t()

A cash-flow amount. This can be any number, or a Decimal when you have that optional dependency installed.

cash_flow()

@type cash_flow() :: {date(), amount()}

A cash flow on a given date. Money coming in is positive, money going out is negative.

date()

@type date() :: Date.t() | {integer(), integer(), integer()}

A date, given either as a Date struct or as an Erlang-style {year, month, day} tuple.

error()

@type error() ::
  :mismatched_lengths
  | :insufficient_data
  | :single_signed_flow
  | :invalid_date
  | :did_not_converge
  | :undefined

option()

@type option() ::
  {:guess, number()}
  | {:tolerance, number()}
  | {:max_iterations, pos_integer()}
  | {:precision, non_neg_integer()}

rate()

@type rate() :: float()

An annual rate of return expressed as a fraction, so 0.1 means 10%.

Functions

db(cost, salvage, life, period, month \\ 12)

@spec db(number(), number(), number(), number(), number()) ::
  {:ok, float()} | {:error, error()}

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}

db!(cost, salvage, life, period, month \\ 12)

@spec db!(number(), number(), number(), number(), number()) :: float()

Same as db/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

ddb(cost, salvage, life, period, factor \\ 2)

@spec ddb(number(), number(), number(), number(), number()) ::
  {:ok, float()} | {:error, error()}

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}

ddb!(cost, salvage, life, period, factor \\ 2)

@spec ddb!(number(), number(), number(), number(), number()) :: float()

Same as ddb/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

fv(rate, nper, pmt, pv \\ 0.0, type \\ 0)

@spec fv(number(), number(), number(), number(), 0 | 1) ::
  {:ok, float()} | {:error, error()}

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

fv!(rate, nper, pmt, pv \\ 0.0, type \\ 0)

@spec fv!(number(), number(), number(), number(), 0 | 1) :: float()

Same as fv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

irr(amounts)

@spec irr([amount()]) :: {:ok, rate()} | {:error, error()}

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}

irr(amounts, opts)

@spec irr([amount()], [option()]) :: {:ok, rate()} | {:error, error()}

Same as irr/1, and additionally takes the same options as xirr/2.

irr!(amounts, opts \\ [])

@spec irr!([amount()], [option()]) :: rate()

Same as irr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.

mirr(amounts, finance_rate, reinvest_rate, opts \\ [])

@spec mirr([amount()], number(), number(), [option()]) ::
  {:ok, rate()} | {:error, error()}

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}

mirr!(amounts, finance_rate, reinvest_rate, opts \\ [])

@spec mirr!([amount()], number(), number(), [option()]) :: rate()

Same as mirr/3, but hands back the rate on its own and raises ArgumentError if the calculation fails.

nper(rate, pmt, pv, fv \\ 0.0, type \\ 0)

@spec nper(number(), number(), number(), number(), 0 | 1) ::
  {:ok, float()} | {:error, error()}

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

nper!(rate, pmt, pv, fv \\ 0.0, type \\ 0)

@spec nper!(number(), number(), number(), number(), 0 | 1) :: float()

Same as nper/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

npv(rate, amounts)

@spec npv(rate(), [amount()]) :: {:ok, number()} | {:error, error()}

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}

npv(rate, amounts, opts)

@spec npv(rate(), [amount()], [option()]) :: {:ok, number()} | {:error, error()}

Same as npv/2, and additionally takes a :precision option. See npv/2.

npv!(rate, amounts)

@spec npv!(rate(), [amount()]) :: number()

Same as npv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.

pmt(rate, nper, pv, fv \\ 0.0, type \\ 0)

@spec pmt(number(), number(), number(), number(), 0 | 1) ::
  {:ok, float()} | {:error, error()}

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

pmt!(rate, nper, pv, fv \\ 0.0, type \\ 0)

@spec pmt!(number(), number(), number(), number(), 0 | 1) :: float()

Same as pmt/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

pv(rate, nper, pmt, fv \\ 0.0, type \\ 0)

@spec pv(number(), number(), number(), number(), 0 | 1) ::
  {:ok, float()} | {:error, error()}

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

pv!(rate, nper, pmt, fv \\ 0.0, type \\ 0)

@spec pv!(number(), number(), number(), number(), 0 | 1) :: float()

Same as pv/5, but hands back the value on its own and raises ArgumentError if the calculation fails.

rate(nper, pmt, pv, fv \\ 0.0, type \\ 0, opts \\ [])

@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}

rate!(nper, pmt, pv, fv \\ 0.0, type \\ 0, opts \\ [])

@spec rate!(number(), number(), number(), number(), 0 | 1, [option()]) :: rate()

Same as rate/6, but hands back the rate on its own and raises ArgumentError if the calculation fails.

sln(cost, salvage, life)

@spec sln(number(), number(), number()) :: {:ok, float()} | {:error, error()}

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}

sln!(cost, salvage, life)

@spec sln!(number(), number(), number()) :: float()

Same as sln/3, but hands back the value on its own and raises ArgumentError if the calculation fails.

syd(cost, salvage, life, period)

@spec syd(number(), number(), number(), number()) ::
  {:ok, float()} | {:error, error()}

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}

syd!(cost, salvage, life, period)

@spec syd!(number(), number(), number(), number()) :: float()

Same as syd/4, but hands back the value on its own and raises ArgumentError if the calculation fails.

volatility(prices, opts \\ [])

@spec volatility(
  [number()],
  keyword()
) :: {:ok, float()} | {:error, error()}

Annualised volatility of a price series — the standard deviation of its period-over-period returns, scaled up to a yearly figure.

Give it a list of prices in time order (daily closes, say). It measures the return between each consecutive pair, takes their sample standard deviation, and annualises by √periods_per_year. At least three prices are needed, and every price must be positive.

iex> Finance.volatility([100, 102, 101, 103, 105])
{:ok, 0.234528}

Options

  • :periods_per_year (pos_integer/0) - number of periods in a year, used to annualise (252 trading days by default) The default value is 252.

  • :returns - how to measure each period's return: :simple (b - a) / a or :log ln(b / a) The default value is :simple.

  • :precision (non_neg_integer/0) - decimal places the result is rounded to The default value is 6.

volatility!(prices, opts \\ [])

@spec volatility!(
  [number()],
  keyword()
) :: float()

Same as volatility/2, but hands back the value on its own and raises ArgumentError if it can't be computed.

xirr(cash_flows)

@spec xirr([cash_flow()]) :: {:ok, rate()} | {:error, error()}

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}

xirr(first, second)

@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}

xirr(dates, values, opts)

@spec xirr([date()], [amount()], [option()]) :: {:ok, rate()} | {:error, error()}

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}

xirr!(cash_flows)

@spec xirr!([cash_flow()]) :: rate()

Same as xirr/1, but hands back the rate on its own and raises ArgumentError if the calculation fails.

xirr!(first, second)

@spec xirr!([cash_flow()] | [date()], [option()] | [amount()]) :: rate()

Same as xirr/2, but hands back the rate on its own and raises ArgumentError if the calculation fails.

xnpv(rate, cash_flows)

@spec xnpv(rate(), [cash_flow()]) :: {:ok, number()} | {:error, error()}

Computes the XNPV — the net present value of a set of dated cash flows, discounted back at rate.

Σ cf_i / (1 + rate)^t_i

Each 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}

xnpv(rate, cash_flows, opts)

@spec xnpv(rate(), [cash_flow()], [option()]) :: {:ok, number()} | {:error, error()}

Same as xnpv/2, and additionally takes a :precision option. See xnpv/2.

xnpv!(rate, cash_flows)

@spec xnpv!(rate(), [cash_flow()]) :: number()

Same as xnpv/2, but hands back the value on its own and raises ArgumentError if the calculation fails.