Packages

Cash-flow analysis for Elixir: XIRR/IRR, net present value, modified IRR, time-value-of-money, and depreciation.

Current section

Files

Jump to
finance lib finance.ex
Raw

lib/finance.ex

defmodule Finance do
use Timex
@moduledoc """
Library to calculate IRR through the Bisection method.
"""
@type rate :: float
@type date :: Date.t
defp pmap(collection, function) do
me = self()
collection
|> Enum.map(fn (element) -> spawn_link fn -> (send me, {self(), function.(element)}) end end)
|> Enum.map(fn (pid) -> receive do {^pid, result} -> result end end)
end
defp xirr_reduction({period, value, rate}),
do: value / :math.pow(1 + rate, period)
@doc """
iex> d = [{2015, 11, 1}, {2015,10,1}, {2015,6,1}]
iex> v = [-800_000, -2_200_000, 1_000_000]
iex> Finance.xirr(d,v)
{ :ok, 21.118359 }
"""
@spec xirr([date], [number]) :: rate
def xirr(dates, values) when length(dates) != length(values) do
{:error, "Date and Value collections must have the same size"}
end
def xirr(dates, values) do
dates = dates
|> pmap(&Date.from_erl!/1)
min_date = Enum.min(dates)
{dates, values, dates_values} = compact_flow(Enum.zip(dates, values), min_date)
cond do
!verify_flow(values) ->
{:error, "Values should have at least one positive or negative value."}
length(dates) - length(values) == 0 && verify_flow(values) ->
calculate :xirr, dates_values, [], guess_rate(dates, values), -1.0, +1.0, 0
true -> {:error, "Uncaught error"}
end
end # def xirr
defp compact_flow(dates_values, min_date) do
flow = Enum.reduce(dates_values, %{}, &organize_value(&1, &2, min_date))
{Map.keys(flow), Map.values(flow), Enum.filter(flow, &(elem(&1,1) != 0))}
end
defp organize_value({date, value}, map, min_date) do
Map.update(map, Timex.diff(date, min_date, :days) / 365.0 , value, &(value + &1))
end
defp verify_flow(values) do
Enum.any?(values, fn(x) -> x > 0 end) &&
Enum.any?(values, fn(x) -> x < 0 end)
end
@spec guess_rate([date], [number]) :: rate
defp guess_rate(dates, values) do
{min_value, max_value} = Enum.min_max(values)
period = 1 / (length(dates) - 1)
multiple = 1 + abs(max_value / min_value)
rate = :math.pow(multiple, period) - 1
Float.round(rate, 3)
end
defp reached_boundry(rate, upper),
do: abs(Float.round(rate - upper, 2)) == 0.0
defp first_value_sign(dates_values) do
[head | _] = dates_values
{_, first_value} = head
cond do
first_value < 0 -> 1
first_value > 0 -> -1
true -> 0
end
end
defp reduce_date_values(dates_values, rate) do
list = dates_values
acc = list
|> pmap(fn (x) ->
{
elem(x,0),
elem(x,1),
rate
} end)
|> pmap(&(xirr_reduction/1))
|> Enum.sum
|> Float.round(4)
acc * first_value_sign(dates_values)
end
defp calculate(:xirr, _ , 0.0 , rate, _ , _ , _), do: {:ok, Float.round(rate,6)}
defp calculate(:xirr, _ , _ , -1.0, _ , _ , _), do: {:error, "Could not converge"}
# defp calculate(:xirr, _ , _ , _ , _ , _ , 300), do: {:error, "I give up"}
defp calculate(:xirr, dates_values, _ , rate, bottom, upper , tries) do
acc = reduce_date_values(dates_values, rate)
{rate, bottom, upper} = cond do
acc < 0 ->
# upper = rate
# rate = (bottom + rate) / 2
{(bottom + rate) / 2, bottom, rate}
acc > 0 && reached_boundry(rate, upper) ->
# bottom = rate
# rate = (rate + upper) / 2
# upper = upper + 1
{(rate + upper) / 2, rate, upper + 1}
acc > 0 && !reached_boundry(rate, upper) ->
# bottom = rate
# rate = (rate + upper) / 2
{(rate + upper) / 2, rate, upper}
acc == 0.0 ->
# rate
{rate, bottom, upper}
end
tries = tries + 1
calculate :xirr, dates_values, acc, rate, bottom, upper, tries
end
end # defmodule Finance