Skip to content

Commit 49340d1

Browse files
author
Shubham Gupta
committed
Add travis file and tests
1 parent 706e74b commit 49340d1

12 files changed

+232
-140
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ erl_crash.dump
77
*.ez
88
*.swp
99
graphs/*
10+
snapshots/*

.travis.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: elixir
2+
3+
elixir:
4+
- '1.0.5'
5+
otp_release: '17.4'
6+
7+
matrix:
8+
include:
9+
- elixir: '1.6'
10+
otp_release: '18.0'
11+
script:
12+
- MIX_ENV=test mix do compile, coveralls.json
13+
14+
after_success:
15+
- bash <(curl -s https://codecov.io/bash)

README.md

+42-16
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,49 @@
1-
# Finance
1+
# ExXirr
2+
[![codecov](https://codecov.io/gh/scripbox/ex-xirr/branch/master/graph/badge.svg)](https://codecov.io/gh/scripbox/ex-xirr) ![Hex.pm](https://img.shields.io/hexpm/dt/snowplow_tracker.svg) ![Travis](https://img.shields.io/travis/scripbox/ex-xirr.svg)
23

3-
A library to calculate Xirr through the newton raphson using parallel processes.
4+
A library to calculate xirr through the Newton Raphson using parallel processes.
45

5-
## Installation
6-
7-
The package can be installed as:
6+
## Usage
87

9-
1. Add finance to your list of dependencies in `mix.exs`:
8+
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
9+
iex> v = [1000, -600, -200]
10+
iex> ExXirr.xirr(d,v)
11+
{:ok, -0.034592}
1012

11-
def deps do
12-
[{:finance, "~> 0.0.1"}]
13-
end
1413

15-
2. Ensure finance is started before your application:
16-
17-
def application do
18-
[applications: [:finance]]
19-
end
14+
## Installation
2015

21-
## Documentation
16+
The package can be installed as:
2217

23-
The Documentation is available in [hexdocs.pm](http://hexdocs.pm/finance/0.0.1/extra-api-reference.html)
18+
1. Add ex_xirr to your list of dependencies in `mix.exs`:
19+
20+
```ex
21+
def deps do
22+
[{:ex_xirr, "~> 1.0.0"}]
23+
end
24+
```
25+
26+
2. Ensure ex_xirr is started before your application:
27+
28+
```ex
29+
def application do
30+
[applications: [:ex_xirr]]
31+
end
32+
```
33+
## Test
34+
35+
- Run the test suite using the following
36+
```
37+
MIX_ENV=test mix test
38+
```
39+
40+
- We use benchfella for the benchmark tests. Run it using the following
41+
```
42+
MIX_ENV=test mix bench
43+
MIX_ENV=test mix bench
44+
# Generate graphs of the above runs
45+
MIX_ENV=test mix bench.graph
46+
```
47+
## Credits
48+
49+
This application is built on the fantastic [finance-elixir](https://github.com/tubedude/finance-elixir) package. Many thanks to [tubedude](https://github.com/tubedude) for his work.

bench/basic_bench.exs

-27
This file was deleted.

bench/xirr_comparison_bench.exs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule XirrComparisonBench do
2+
use Benchfella
3+
4+
@v [
5+
105187.06, 816709.66, 479069.684, 937309.708, 88622.661, 100000.0, 80000.0, 403627.95, 508117.9, 789706.87,
6+
-88622.661, -789706.871,-688117.9, -403627.95, 403627.95, 789706.871, 88622.661, 688117.9, 45129.14, 26472.08,
7+
51793.2, 126605.59, 278532.29, 99284.1, 58238.57, 113945.03, 405137.88, -405137.88, 165738.23, -165738.23,
8+
144413.24, 84710.65, -84710.65, -144413.24
9+
]
10+
11+
@d [
12+
{2011,12,07},{2011,12,07},{2011,12,07},{2012,01,18},{2012,07,03},{2012,07,03},{2012,07,19},{2012,07,23},
13+
{2012,07,23},{2012,07,23}, {2012,09,11},{2012,09,11},{2012,09,11},{2012,09,11},{2012,09,12},{2012,09,12},
14+
{2012,09,12},{2012,09,12},{2013,03,11},{2013,03,11},{2013,03,11},{2013,03,11},{2013,03,28},{2013,03,28},
15+
{2013,03,28},{2013,03,28},{2013,05,21},{2013,05,21},{2013,05,21},{2013,05,21},{2013,05,21},{2013,05,21},
16+
{2013,05,21},{2013,05,21}
17+
]
18+
19+
bench "long investment bisection method" do
20+
LegacyFinance.xirr(@d,@v)
21+
end
22+
23+
bench "long investment newton method" do
24+
ExXirr.xirr(@d,@v)
25+
end
26+
end

lib/finance.ex renamed to lib/ex_xirr.ex

+78-58
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,26 @@
1-
defmodule Finance do
1+
defmodule ExXirr do
22
@moduledoc """
3-
Library to calculate XIRR through the Newton Raphson method.
3+
Library to calculate XIRR and absolute rate of return
4+
through the Newton Raphson method.
45
"""
56

6-
@type date :: Date.t()
7-
87
@max_error 1.0e-3
98
@days_in_a_year 365
109

11-
defp pmap(collection, function) do
12-
me = self
13-
14-
collection
15-
|> Enum.map(fn element -> spawn_link(fn -> send(me, {self, function.(element)}) end) end)
16-
|> Enum.map(fn pid ->
17-
receive do
18-
{^pid, result} -> result
19-
end
20-
end)
21-
end
22-
23-
@spec power_of(float(), Fraction.t()) :: float()
24-
def power_of(rate, fraction) when rate < 0 do
25-
:math.pow(-rate, Fraction.to_float(fraction)) * :math.pow(-1, fraction.num)
26-
end
10+
# Public API
2711

28-
def power_of(rate, fraction) do
29-
:math.pow(rate, Fraction.to_float(fraction))
30-
end
31-
32-
@spec xirr_reduction({Fraction.t(), float(), float()}) :: float()
33-
defp xirr_reduction({fraction, value, rate}) do
34-
value / power_of(1.0 + rate, fraction)
35-
end
12+
@doc """
13+
Function to calculate the rate of return for a given array of
14+
dates and values.
3615
37-
@spec dxirr_reduction({Fraction.t(), float(), float()}) :: float()
38-
defp dxirr_reduction({fraction, value, rate}) do
39-
-value * Fraction.to_float(fraction) * power_of(1.0 + rate, Fraction.negative(fraction)) *
40-
:math.pow(1.0 + rate, -1)
41-
end
16+
## Examples
4217
43-
@doc """
44-
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
45-
iex> v = [1000, -600, -200]
46-
iex> Finance.xirr(d,v)
47-
{:ok, -0.034592}
18+
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
19+
iex> v = [1000, -600, -200]
20+
iex> ExXirr.xirr(d,v)
21+
{:ok, -0.034592}
4822
"""
49-
@spec xirr([date], [number]) :: float
23+
@spec xirr([Date.t()], [number]) :: float
5024
def xirr(dates, values) when length(dates) != length(values) do
5125
{:error, "Date and Value collections must have the same size"}
5226
end
@@ -75,8 +49,20 @@ defmodule Finance do
7549
{:error, 0.0}
7650
end
7751

52+
@doc """
53+
Function to calculate the absolute rate of return for a given array
54+
of dates and values.
55+
56+
## Examples
57+
58+
iex> d = [{1985, 1, 1}, {1990, 1, 1}, {1995, 1, 1}]
59+
iex> v = [1000, -600, -200]
60+
iex> {:ok, rate} = ExXirr.xirr(d,v)
61+
iex> ExXirr.absolute_rate(rate, 50)
62+
{:ok, -0.48}
63+
"""
7864
@spec absolute_rate(float(), integer()) :: {:ok, float()} | {:error, String.t()}
79-
def absolute_rate(0, days), do: {:error, "Rate is 0"}
65+
def absolute_rate(0, _), do: {:error, "Rate is 0"}
8066

8167
def absolute_rate(rate, days) do
8268
try do
@@ -91,11 +77,48 @@ defmodule Finance do
9177
end
9278
end
9379

80+
# Private API
81+
82+
@spec pmap(list(tuple()), fun()) :: Enum.t()
83+
defp pmap(collection, function) do
84+
me = self()
85+
86+
collection
87+
|> Enum.map(fn element -> spawn_link(fn -> send(me, {self(), function.(element)}) end) end)
88+
|> Enum.map(fn pid ->
89+
receive do
90+
{^pid, result} -> result
91+
end
92+
end)
93+
end
94+
95+
@spec power_of(float(), Fraction.t()) :: float()
96+
defp power_of(rate, fraction) when rate < 0 do
97+
:math.pow(-rate, Fraction.to_float(fraction)) * :math.pow(-1, fraction.num)
98+
end
99+
100+
defp power_of(rate, fraction) do
101+
:math.pow(rate, Fraction.to_float(fraction))
102+
end
103+
104+
@spec xirr_reduction({Fraction.t(), float(), float()}) :: float()
105+
defp xirr_reduction({fraction, value, rate}) do
106+
value / power_of(1.0 + rate, fraction)
107+
end
108+
109+
@spec dxirr_reduction({Fraction.t(), float(), float()}) :: float()
110+
defp dxirr_reduction({fraction, value, rate}) do
111+
-value * Fraction.to_float(fraction) * power_of(1.0 + rate, Fraction.negative(fraction)) *
112+
:math.pow(1.0 + rate, -1)
113+
end
114+
115+
@spec compact_flow(list(), Date.t()) :: tuple()
94116
defp compact_flow(dates_values, min_date) do
95117
flow = Enum.reduce(dates_values, %{}, &organize_value(&1, &2, min_date))
96118
{Map.keys(flow), Map.values(flow), Enum.filter(flow, &(elem(&1, 1) != 0))}
97119
end
98120

121+
@spec organize_value(tuple(), map(), Date.t()) :: map()
99122
defp organize_value(date_value, dict, min_date) do
100123
{date, value} = date_value
101124

@@ -104,15 +127,16 @@ defmodule Finance do
104127
den: 365.0
105128
}
106129

107-
Dict.update(dict, fraction, value, &(value + &1))
130+
Map.update(dict, fraction, value, &(value + &1))
108131
end
109132

133+
@spec verify_flow(list(float())) :: boolean()
110134
defp verify_flow(values) do
111135
{min, max} = Enum.min_max(values)
112136
min < 0 && max > 0
113137
end
114138

115-
@spec guess_rate([date], [number]) :: float
139+
@spec guess_rate([Date.t()], [number]) :: float
116140
defp guess_rate(dates, values) do
117141
{min_value, max_value} = Enum.min_max(values)
118142
period = 1 / (length(dates) - 1)
@@ -121,11 +145,10 @@ defmodule Finance do
121145
Float.round(rate, 6)
122146
end
123147

148+
@spec reduce_date_values(list(), float()) :: tuple()
124149
defp reduce_date_values(dates_values, rate) do
125-
list = Dict.to_list(dates_values)
126-
127150
calculated_xirr =
128-
list
151+
dates_values
129152
|> pmap(fn x ->
130153
{
131154
elem(x, 0),
@@ -138,7 +161,7 @@ defmodule Finance do
138161
|> Float.round(6)
139162

140163
calculated_dxirr =
141-
list
164+
dates_values
142165
|> pmap(fn x ->
143166
{
144167
elem(x, 0),
@@ -153,28 +176,25 @@ defmodule Finance do
153176
{calculated_xirr, calculated_dxirr}
154177
end
155178

179+
@spec calculate(atom(), list(), float(), float(), integer()) ::
180+
{:ok, float()} | {:error, String.t()}
156181
defp calculate(:xirr, _, 0.0, rate, _), do: {:ok, Float.round(rate, 6)}
157182
defp calculate(:xirr, _, _, -1.0, _), do: {:error, "Could not converge"}
158183
defp calculate(:xirr, _, _, _, 300), do: {:error, "I give up"}
159184

160185
defp calculate(:xirr, dates_values, _, rate, tries) do
161186
{xirr, dxirr} = reduce_date_values(dates_values, rate)
162187

163-
if dxirr < 0.0 do
164-
new_rate = rate
165-
else
166-
new_rate = rate - xirr / dxirr
167-
end
188+
new_rate =
189+
if dxirr < 0.0 do
190+
rate
191+
else
192+
rate - xirr / dxirr
193+
end
168194

169195
diff = Kernel.abs(new_rate - rate)
170-
171-
if diff < @max_error do
172-
diff = 0.0
173-
end
174-
196+
diff = if diff < @max_error, do: 0.0
175197
tries = tries + 1
176198
calculate(:xirr, dates_values, diff, new_rate, tries)
177199
end
178200
end
179-
180-
# defmodule Finance

0 commit comments

Comments
 (0)