Current section
Files
Jump to
Current section
Files
README.md
# Finance
[](https://github.com/tubedude/finance-elixir/actions/workflows/ci.yml)
[](https://hex.pm/packages/finance)
[](https://hexdocs.pm/finance)
[](https://coveralls.io/github/tubedude/finance-elixir?branch=main)
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 functions are organised into domain modules:
- `Finance.CashFlow` — net present/future value and internal rate of return
(`npv`, `xnpv`, `xnfv`, `irr`, `xirr`, `mirr`), `conventional?`, plus batched
`irr_many`/`xirr_many`.
- `Finance.TVM` — time-value-of-money scalars (`pv`, `fv`, `pmt`, `ipmt`, `ppmt`,
`nper`, `rate`) plus `amortization_schedule`.
- `Finance.Rates` — rate conversions (`effective_annual_rate`, `nominal_rate`,
`continuous_to_periodic`).
- `Finance.Bonds` — fixed income (`price`, `ytm`, `duration`,
`modified_duration`, `convexity`).
- `Finance.Depreciation` — `sln`, `syd`, `ddb`, `db`.
- `Finance.Returns` — performance and risk metrics (`volatility`, `cagr`,
`payback_period`, `discounted_payback_period`, `profitability_index`, `twr`).
- `Finance.Solver` — the root-finding strategy behind the rate functions,
swappable via the `:solver` option or `config :finance, solver: MySolver`.
- `Finance.DayCount` — the day-count convention for dated flows, selected with the
`:basis` option; five built-in conventions plus a behaviour for your own.
The `Finance.CashFlow` rate and value functions come in two forms. The **dated**
ones (`xirr`, `xnpv`, `xnfv`) work with flows that land on arbitrary dates,
discounting on an Actual/365 basis by default to match spreadsheet `XIRR`/`XNPV`
(see [Day-count conventions](#day-count-conventions)). 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.
> The flat `Finance.foo` functions (e.g. `Finance.xirr/1`) still work but are
> **deprecated** — call the domain module instead. They will be removed in 2.0.
## Installation
Add `finance` to your dependencies in `mix.exs`:
```elixir
def deps do
[{:finance, "~> 1.8"}]
end
```
If you also want to pass `Decimal` amounts, add `{:decimal, "~> 2.0 or ~> 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.
```elixir
Finance.CashFlow.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:
```elixir
Finance.CashFlow.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:
```elixir
Finance.CashFlow.irr([-1000, 500, 500, 300]) #=> {:ok, 0.156579}
Finance.CashFlow.npv(0.1, [-1000, 600, 600]) #=> {:ok, 41.322314}
Finance.CashFlow.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.
Two companions round out the cash-flow toolkit: `xnfv/2` gives the net *future*
value of dated flows (the mirror of `xnpv`), and `conventional?/1` reports whether
a series changes sign exactly once — a `false` warns that it may have several
valid IRRs before you solve.
### Amounts: numbers, Decimal, and Money
Amounts may be any number — integer minor units such as cents, or floats. If your
app already depends on [`Decimal`](https://hex.pm/packages/decimal), you can pass
`Decimal` values straight through, with no conversion on your side:
```elixir
Finance.CashFlow.xirr([{~D[2019-01-01], Decimal.new("-1000")}, {~D[2020-01-01], Decimal.new("1100")}])
#=> {:ok, 0.1}
```
[`ex_money`](https://hex.pm/packages/ex_money) `%Money{}` values work too — common
when amounts come from an Ecto money column — and here the currency matters:
```elixir
Finance.CashFlow.xirr([{~D[2019-01-01], Money.new(:USD, "-1000")}, {~D[2020-01-01], Money.new(:USD, "1100")}])
#=> {:ok, 0.1}
# A series may not mix currencies:
Finance.CashFlow.irr([Money.new(:USD, "-1000"), Money.new(:EUR, "1100")])
#=> {:error, :mixed_currencies}
```
Both `Decimal` and `ex_money` are optional — apps that don't use them pull in
nothing extra (finance reads a `%Money{}`'s amount without depending on it). Plain
numbers and `Decimal` are currency-neutral, so they never trip the currency check.
Either way the result comes back as a float. The solver works in floats, so
passing `Decimal` or `Money` saves you a conversion; it does not make the answer
more precise.
### Errors
When the data can't produce a result, the functions return `{:error, reason}`
(the `!` variants raise instead). The common reasons are:
| 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 |
| `:undefined` | inputs outside the function's domain, e.g. a rate at or below −100% |
| `:mixed_currencies` | a series mixes two or more `%Money{}` currencies |
## Day-count conventions
By default `xirr`/`xnpv` measure the time between dates as Actual/365 — the same
basis as spreadsheet `XIRR`. Many bonds and loans are quoted under a different
convention, such as 30/360. If you use the wrong one, the result will not match
the term sheet. Pick the convention with `:basis`:
```elixir
Finance.CashFlow.xirr(flows, basis: :thirty_360)
```
Five conventions ship built in: `:actual_365` (default), `:actual_360`,
`:actual_actual` (ISDA), `:thirty_360` (US/NASD), and `:thirty_e_360` (Eurobond).
See `Finance.DayCount`.
`:basis` also takes any module implementing the `Finance.DayCount` behaviour. Use
this for conventions that need a holiday calendar, such as Brazilian Business/252.
This library does not include holiday calendars, so you write the module in your
app:
```elixir
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)
```
If your module needs settings, pass `{module, settings}`. The settings go to
every `year_fraction/3` call:
```elixir
Finance.CashFlow.xirr(flows, basis: {MyApp.ActualActualIcma, frequency: 2})
```
Load the market's published holidays (for example, ANBIMA for Brazil) into a
fixed set of business days, and count against that set. This is better than
working out holidays at runtime. [`ex_tempo`](https://hex.pm/packages/ex_tempo)
can build the set from an `.ics` calendar. See `Finance.DayCount` for details.
## Solver
The rate functions (`irr`, `xirr`, `rate`, `ytm`) find their rate with a
safeguarded Newton-Raphson — the classic `rtsafe`. It brackets the root, then
each step is a Newton step when that step lands inside the bracket and is
converging fast enough, and a bisection step otherwise. This keeps Newton's
speed on ordinary flows and bisection's guaranteed convergence on awkward ones,
in a single pass. Because the maintained bracket always encloses a sign change,
the result is a genuine root rather than a stalled non-root. The solver is
swappable via the `:solver` option or `config :finance, solver: MySolver`.
`Finance.Solver.Brent` ships as an alternative: Brent's method, which is
derivative-free and so spends one NPV evaluation per step instead of two. On
short series the default is quicker, but Brent is faster on long-horizon flows —
long amortization schedules or bond ladders — where each evaluation is expensive.
Pass `solver: Finance.Solver.Brent` where it pays.
`bench/solver_strategies.exs` compares the two shipped solvers against reference
strategies across flow sets of growing length (NPV/derivative evaluations per
solve, and median time):
| flow set | safeguarded Newton (default) | Brent | plain Newton, then bisect | pure bisection |
| --------------- | ---------------------------- | ------------------ | ------------------------- | ----------------- |
| 4 flows | 13 evals · 7.5 µs | 14 evals · 7.4 µs | 8 evals · 3.7 µs | 65 evals · 22 µs |
| 60-period loan | 13 evals · 94 µs | 16 evals · 72 µs | 44 evals · 256 µs | 65 evals · 335 µs |
| 480-period loan | 31 evals · 1.5 ms | 24 evals · 0.86 ms | 265 evals · 11.6 ms | 65 evals · 2.3 ms |
Safeguarded Newton is the default because it is fastest or near-fastest on every
set. `Finance.Solver.Brent` ties it on the shortest flows and pulls
ahead as they lengthen (~1.3× faster on the medium loan, ~1.7× on the long one),
because it spends one evaluation per step instead of two. Plain Newton edges both
out on the tiny set but burns its whole iteration budget on long flows before a
separate bisection pass rescues it (~8× slower than the default). Run it with
`mix run bench/solver_strategies.exs`.
### Batch and the native backend
`Finance.CashFlow.irr_many/2` and `xirr_many/2` solve a whole portfolio in one
call, returning a list of `{:ok, rate}` / `{:error, reason}` in order. They
dispatch through the solver's `solve_many/2`, which the default solver runs in
parallel across schedulers (chunked `Task.async_stream`).
Because the solver is swappable, a batch can run on a **native backend** with no
API change. [`finance_rustler`](https://github.com/tubedude/finance_rustler) is a
Rust (Rustler) backend whose `solve_many/2` runs the whole batch in one call over
a rayon thread pool — add it and point `:solver` at it:
```elixir
# mix.exs
{:finance, "~> 1.8"},
{:finance_rustler, "~> 0.2"}
# config/config.exs
config :finance, solver: FinanceRustler.Solver
```
Its `bench/solve_many.exs` compares the batch strategies — median time to solve a
whole batch:
| batch | native (rayon) | pure (chunked) | sequential |
| ---------------------- | -------------- | -------------- | ---------- |
| 1,000 × 4-flow | 2.4 ms | 7.6 ms | 8.1 ms |
| 1,000 × 60-period loan | 14.8 ms | 23.7 ms | 185 ms |
| 5,000 × 60-period loan | 114 ms | 98 ms | 1,004 ms |
Both parallel strategies beat a sequential map by 10–13×. The native backend is
fastest on batches of small series (~3× on the 4-flow set); the chunked pure
solver pulls even on large, heavier batches and uses far less memory. So the
native backend is an opt-in for throughput and for keeping heavy work off the
BEAM schedulers — not a requirement.
## Development
```bash
mix deps.get
mix test
mix format
mix credo --strict
mix dialyzer
```
See [CHANGELOG.md](CHANGELOG.md) for the release history.