defmodule Finance do alias Timex.Date @moduledoc """ Library to calculate IRR through the Bisection method. """ 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) @type rate :: float @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(list, list) :: rate def xirr(dates, values) do cond do !verify_flow(values) -> {:error, "Values should have at least one positive or negative value."} length(dates) - length(values) != 0 -> {:error, "Date and Value collections size don't match."} length(dates) - length(values) == 0 && verify_flow(values) -> dates = dates |> pmap(&Date.from/1) min_date = Enum.min(dates) {dates, values, dates_values} = compact_flow(Enum.zip(dates, values), Date.to_days(min_date)) 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, dict, min_date) do {date, value} = date_value Dict.update(dict, ((Date.to_days(date) - min_date) / 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(list, list ) :: rate defp guess_rate(dates, values) do {min_value, max_value} = Enum.min_max(values) period = length(dates) - 1 Float.round(:math.pow(1 + abs(max_value / min_value) , 1 / period) - 1 , 3) end defp reached_boundry(rate, upper), do: abs(Float.round(rate - upper, 2)) == 0.0 defp first_value_sign(dates_values) do [head | _] = Dict.to_list(dates_values) {_, first_value} = head cond do first_value < 0 -> 1 first_value > 0 -> -1 end end defp reduce_date_values(dates_values, rate) do acc = Dict.to_list(dates_values) |> 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) # IO.inspect "#{acc}; #{rate}; #{bottom}; #{upper}" cond do acc < 0 -> upper = rate rate = (bottom + rate) / 2 acc > 0 && reached_boundry(rate, upper) -> bottom = rate rate = (rate + upper) / 2 upper = upper + 1 acc > 0 && !reached_boundry(rate, upper) -> bottom = rate rate = (rate + upper) / 2 acc == 0.0 -> rate end tries = tries + 1 calculate :xirr, dates_values, acc, rate, bottom, upper, tries end end # defmodule Finance