Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Previous Ex-Xirr Fixes #1

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 27 additions & 19 deletions lib/ex_xirr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExXirr do
through the Newton Raphson method.
"""

@max_error 1.0e-3
@delta 0.00000001
@days_in_a_year 365

# Public API
Expand All @@ -25,7 +25,7 @@ defmodule ExXirr do
{:error, "Date and Value collections must have the same size"}
end

def xirr(dates, values) when length(dates) < 10 do
def xirr(dates, values) when length(dates) < 7 do
LegacyFinance.xirr(dates, values)
rescue
e in _ ->
Expand All @@ -35,6 +35,7 @@ defmodule ExXirr do
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)
Expand All @@ -44,7 +45,13 @@ defmodule ExXirr do
{: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), 0)
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"}
Expand All @@ -66,17 +73,17 @@ defmodule ExXirr do
iex> v = [1000, -600, -200]
iex> {:ok, rate} = ExXirr.xirr(d,v)
iex> ExXirr.absolute_rate(rate, 50)
{:ok, -0.48}
{: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(2)}
{:ok, ((:math.pow(1 + rate, days / @days_in_a_year) - 1) * 100) |> Float.round(6)}
else
{:ok, (rate * 100) |> Float.round(2)}
{:ok, (rate * 100) |> Float.round(8)}
end
rescue
e in _ ->
Expand Down Expand Up @@ -138,7 +145,7 @@ defmodule ExXirr do
period = 1 / (length(dates) - 1)
multiple = 1 + abs(max_value / min_value)
rate = :math.pow(multiple, period) - 1
Float.round(rate, 6)
Float.round(rate, 8)
end

@spec reduce_date_values(list(), float()) :: tuple()
Expand All @@ -154,7 +161,7 @@ defmodule ExXirr do
end)
|> Enum.map(&xirr_reduction/1)
|> Enum.sum()
|> Float.round(6)
|> Float.round(8)

calculated_dxirr =
dates_values
Expand All @@ -167,30 +174,31 @@ defmodule ExXirr do
end)
|> Enum.map(&dxirr_reduction/1)
|> Enum.sum()
|> Float.round(6)
|> Float.round(8)

{calculated_xirr, calculated_dxirr}
end

@spec calculate(atom(), list(), float(), float(), integer()) ::
{:ok, float()} | {:error, String.t()}
defp calculate(:xirr, _, acc, rate, _) when acc in [-0.0, +0.0], 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, _, _, rate, _) when rate > 1_000_000_000,
do: {:error, "Converged on infinity."}

defp calculate(:xirr, _, acc, rate, _) when acc in [-0.0, +0.0], 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 dxirr < 0.0 do
rate
else
rate - xirr / dxirr
end
new_rate = if Kernel.abs(dxirr) < @delta, do: rate, else: rate - xirr / dxirr

diff = Kernel.abs(new_rate - rate)
diff = if diff < @max_error, do: 0.0
diff = if diff < @delta, do: 0.0
tries = tries + 1

calculate(:xirr, dates_values, diff, new_rate, tries)
end
end
86 changes: 66 additions & 20 deletions test/ex_xirr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,69 @@ defmodule ExXirrTest do
assert ExXirr.xirr(d, v) == {:ok, 0.225683}
end

test "long investment" do
test "longterm investment" do
v = [
-105_187.06,
-816_709.66,
-479_069.684,
-937_309.708,
-88622.661,
100_000.0,
80000.0,
403_627.95,
508_117.9,
789_706.87,
88622.661,
789_706.871,
688_117.9,
-403_627.95,
-403_627.95,
-789_706.871,
88622.661,
688_117.9,
45129.14,
26472.08,
51793.2,
126_605.59,
278_532.29,
99284.1,
58238.57,
113_945.03
]

d = [
{2011, 01, 07},
{2011, 06, 07},
{2011, 09, 07},
{2012, 01, 18},
{2012, 02, 03},
{2012, 03, 03},
{2012, 04, 19},
{2012, 05, 23},
{2012, 06, 23},
{2012, 07, 23},
{2012, 08, 11},
{2012, 09, 11},
{2012, 10, 11},
{2012, 11, 11},
{2012, 12, 12},
{2013, 01, 12},
{2013, 02, 12},
{2013, 03, 12},
{2013, 04, 11},
{2013, 05, 11},
{2013, 06, 11},
{2013, 07, 11},
{2013, 08, 28},
{2013, 09, 28},
{2013, 10, 28},
{2013, 12, 28}
]

assert ExXirr.xirr(d, v) == {:ok, 0.39132547}
end

test "converge on infinity" do
v = [
105_187.06,
816_709.66,
Expand All @@ -64,15 +126,7 @@ defmodule ExXirrTest do
278_532.29,
99284.1,
58238.57,
113_945.03,
405_137.88,
-405_137.88,
165_738.23,
-165_738.23,
144_413.24,
84710.65,
-84710.65,
-144_413.24
113_945.03
]

d = [
Expand Down Expand Up @@ -101,18 +155,10 @@ defmodule ExXirrTest do
{2013, 03, 28},
{2013, 03, 28},
{2013, 03, 28},
{2013, 03, 28},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21},
{2013, 05, 21}
{2013, 03, 28}
]

assert ExXirr.xirr(d, v) == {:ok, 0.08006}
assert ExXirr.xirr(d, v) == {:error, "Could not converge"}
end

test "wrong size" do
Expand Down