defmodule Xirr do
@moduledoc """
Library to calculate XIRR and absolute rate of return
through the Newton Raphson method.
"""
@delta 0.00000001
@days_in_a_year 365
# Public API
@doc """
Function to calculate the rate of return for a given array of
dates and values.
## Examples
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
iex> v = [1000, -600, -200]
iex> Xirr.xirr(d,v)
{:ok, -0.034592}
"""
@spec xirr([Date.t()], [number]) :: float
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) when length(dates) < 7 do
LegacyFinance.xirr(dates, values)
end
def xirr(dates, values) do
{legacy_dates, legacy_values} = {dates, values}
dates = Enum.map(dates, &Date.from_erl!(&1))
min_date = dates |> List.first()
{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) ->
case calculate(:xirr, dates_values, -1.0, guess_rate(dates, values), 0) do
{:ok, xirr} ->
{:ok, xirr}
{:error, _} ->
LegacyFinance.xirr(legacy_dates, legacy_values)
end
true ->
{:error, "Uncaught error"}
end
rescue
_ ->
{:error, 0.0}
end
@doc """
Function to calculate the absolute rate of return for a given array
of dates and values.
## Examples
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
iex> v = [1000, -600, -200]
iex> {:ok, rate} = Xirr.xirr(d,v)
iex> Xirr.absolute_rate(rate, 50)
{:ok, -0.481092}
"""
@spec absolute_rate(float(), integer()) :: {:ok, float()} | {:error, String.t()}
def absolute_rate(0, _), do: {:error, "Rate is 0"}
def absolute_rate(rate, days) do
try do
if days < @days_in_a_year do
{:ok, ((:math.pow(1 + rate, days / @days_in_a_year) - 1) * 100) |> Float.round(6)}
else
{:ok, (rate * 100) |> Float.round(8)}
end
rescue
_ ->
{:error, 0.0}
end
end
# Private API
@spec pmap(list(tuple()), fun()) :: Enum.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
@spec power_of(float(), Fraction.t()) :: float()
defp power_of(rate, fraction) when rate < 0 do
:math.pow(-rate, Fraction.to_float(fraction)) * :math.pow(-1, fraction.num)
end
defp power_of(rate, fraction) do
:math.pow(rate, Fraction.to_float(fraction))
end
@spec xirr_reduction({Fraction.t(), float(), float()}) :: float()
defp xirr_reduction({fraction, value, rate}) do
value / power_of(1.0 + rate, fraction)
end
@spec dxirr_reduction({Fraction.t(), float(), float()}) :: float()
defp dxirr_reduction({fraction, value, rate}) do
-value * Fraction.to_float(fraction) * power_of(1.0 + rate, Fraction.negative(fraction)) *
:math.pow(1.0 + rate, -1)
end
@spec compact_flow(list(), Date.t()) :: tuple()
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
@spec organize_value(tuple(), map(), Date.t()) :: map()
defp organize_value(date_value, dict, min_date) do
{date, value} = date_value
fraction = %Fraction{
num: Date.diff(date, min_date),
den: 365.0
}
Map.update(dict, fraction, value, &(value + &1))
end
@spec verify_flow(list(float())) :: boolean()
defp verify_flow(values) do
{min, max} = Enum.min_max(values)
min < 0 && max > 0
end
@spec guess_rate([Date.t()], [number]) :: float
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, 8)
end
@spec reduce_date_values(list(), float()) :: tuple()
defp reduce_date_values(dates_values, rate) do
calculated_xirr =
dates_values
|> pmap(fn x ->
{
elem(x, 0),
elem(x, 1),
rate
}
end)
|> pmap(&xirr_reduction/1)
|> Enum.sum()
|> Float.round(8)
calculated_dxirr =
dates_values
|> pmap(fn x ->
{
elem(x, 0),
elem(x, 1),
rate
}
end)
|> pmap(&dxirr_reduction/1)
|> Enum.sum()
|> Float.round(8)
{calculated_xirr, calculated_dxirr}
end
@spec calculate(atom(), list(), float(), float(), integer()) ::
{:ok, float()} | {:error, String.t()}
defp calculate(:xirr, _, _, rate, _) when rate > 1_000_000_000,
do: {:error, "Converged on infinity."}
defp calculate(:xirr, _, 0.0, rate, _), do: {:ok, Float.round(rate, 8)}
defp calculate(:xirr, _, _, -1.0, _), do: {:error, "Could not converge."}
defp calculate(:xirr, _, _, _, 500), do: {:error, "Did not converge after 500 iterations."}
defp calculate(:xirr, dates_values, _, rate, tries) do
{xirr, dxirr} = reduce_date_values(dates_values, rate)
new_rate = if Kernel.abs(dxirr) < @delta, do: rate, else: rate - xirr / dxirr
diff = Kernel.abs(new_rate - rate)
diff = if diff < @delta, do: 0.0
tries = tries + 1
calculate(:xirr, dates_values, diff, new_rate, tries)
end
end