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 xirr_reduction({period, value, rate}), do: value / :math.pow(1 + rate, period)

  @doc """
  Function to calculate the XIRR for a given array of dates and values.

  ## Examples
      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
      |> Enum.map(&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) ->
        boundries = {guess_rate(dates, values), -1.0, +1.0}
        calculate(:xirr, dates_values, [], boundries, 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
    days = Timex.diff(date, min_date, :days) / 365.0
    Map.update(map, days, 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
      |> Enum.map(fn x ->
        {
          elem(x, 0),
          elem(x, 1),
          rate
        }
      end)
      |> Enum.map(&xirr_reduction/1)
      |> Enum.sum()
      |> Float.round(4)

    acc * first_value_sign(dates_values)
  end

  defp calculate(:xirr, _date_values, 0.0, {rate, _bottom, _upper}, _tries) do
    {:ok, Float.round(rate, 6)}
  end

  defp calculate(:xirr, _date_values, _acc, {-1.0, _bottom, _upper}, _tries) do
    {:error, "Could not converge"}
  end

  defp calculate(:xirr, _date_values, _acc, {_, _, _}, 300) do
    {:error, "Unable to converge"}
  end

  defp calculate(:xirr, dates_values, _acc, {rate, bottom, upper}, tries) do
    acc = reduce_date_values(dates_values, rate)

    resp =
      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, resp, tries)
  end
end