financial_calculator 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ module FinancialCalculator
2
+ # Calculate the net present value of a series of unequally spaced, (potentially) unequal cashflows
3
+ class Xnpv
4
+ # @return [Numeric] The rates used for calculating the present value
5
+ # @api public
6
+ attr_reader :rate
7
+
8
+ # @return [Array<Numeric>] An array of cashflow amounts
9
+ # @api public
10
+ attr_reader :cashflows
11
+
12
+ # @return [Array<Date>] An array of dates on which the cashflows occur
13
+ # @api public
14
+ attr_reader :dates
15
+
16
+ # @return [DecNum] Result of the XNPV calculation
17
+ # @api public
18
+ attr_reader :result
19
+
20
+ # Allows for creating an XNPV calculation with an array of any objects that respond to #amount and #date
21
+ # @param [Numeric] rate The discount (interest) rate
22
+ # @param [Array<Transaction>] transaction An array of Transaction objects.
23
+ # @return [FinancialCalculator::Xnpv]
24
+ # @api public
25
+ def self.with_transactions(rate, transactions)
26
+ raise ArgumentError.new("Argument \"rate\" must be a Numeric. Got #{rate.class} instead") unless rate.is_a? Numeric
27
+ unless transactions.all? { |t| t.is_a? Transaction }
28
+ raise ArgumentError.new("Argument \"transactions\" must be an array of Transaction")
29
+ end
30
+
31
+ cashflows = transactions.map(&:amount)
32
+ dates = transactions.map(&:date)
33
+
34
+ self.new(rate, cashflows, dates)
35
+ end
36
+
37
+ # Create a new XNPV calculation
38
+ # @param [Numeric] rate The discount (interest) rate
39
+ # @param [Array<Numeric>] cashflows An array of cashflows
40
+ # @param [Array<Date>] dates An array of dates representing when each cashflow occurs
41
+ # @return [FinancialCalculator::Xnpv]
42
+ # @raise [ArgumentError] When cashflows and dates are not the same length
43
+ # @raise [ArgumentError] When dates is not an array of Date
44
+ # @api public
45
+ def initialize(rate, cashflows, dates)
46
+ validate_cashflows_dates_size(cashflows, dates)
47
+ validate_dates(dates)
48
+
49
+ @rate = Flt::DecNum(rate.to_s)
50
+ @cashflows = cashflows.map { |cashflow| Flt::DecNum(cashflow.to_s) }.freeze
51
+ @dates = dates.freeze
52
+ @result = solve(rate, cashflows, dates)
53
+ end
54
+
55
+ # @api public
56
+ def inspect
57
+ "XNPV(#{result})"
58
+ end
59
+
60
+ private
61
+
62
+ # Solve the NPV calcluation.
63
+ # @note This methods uses a 365 day year regardless of whether or not the
64
+ # year is actually a leap year. This is done in order to maintain
65
+ # consistency with common implementations such as Excel and Google Sheets.
66
+ # In the future it would be nice to have a flag that allows for using
67
+ # the actual number of days in the year
68
+ def solve(rate, cashflows, dates)
69
+ start = dates[0]
70
+ transactions = cashflows.zip(dates)
71
+ amount_index = 0
72
+ date_index = 1
73
+
74
+ transactions.reduce(0) do |total, transaction|
75
+ total += transaction[amount_index] / ((1 + @rate) ** (Flt::DecNum(transaction[date_index] - start) / 365))
76
+ end
77
+ end
78
+
79
+ def validate_dates(dates)
80
+ unless dates.all? { |date| date.is_a? Date }
81
+ raise ArgumentError.new("Argument dates must be an array of Date")
82
+ end
83
+ end
84
+
85
+ def validate_cashflows_dates_size(cashflows, dates)
86
+ unless dates.length == cashflows.length
87
+ raise ArgumentError.new("Arguments cashflows and dates must be the same length")
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Irr" do
4
+ let(:flows) { [-10, 20, 30, 40] }
5
+ let(:irr) { Irr.new(flows) }
6
+
7
+ describe '#result' do
8
+ subject { irr.result }
9
+
10
+ it { is_expected.to be_a Flt::DecNum }
11
+
12
+ context 'when all values are positive' do
13
+ let(:flows) { [10, 20, 30, 40] }
14
+ it_behaves_like 'the values do not converge'
15
+ end
16
+
17
+ context 'when all values are negative' do
18
+ let(:flows) { [-10, -20, -30, -40] }
19
+ it_behaves_like 'the values do not converge'
20
+ end
21
+
22
+ context 'when the leading value is zero' do
23
+ describe 'all other values are positive' do
24
+ let(:flows) { [0, 10, 20, 30] }
25
+ it_behaves_like 'the values do not converge'
26
+ end
27
+
28
+ describe 'all other values are negative' do
29
+ let(:flows) { [0, -10, -20, -30] }
30
+ it_behaves_like 'the values do not converge'
31
+ end
32
+ end
33
+
34
+ describe '#inspect' do
35
+ subject { irr.inspect }
36
+
37
+ it { is_expected.to be_a String }
38
+ it { is_expected.to include 'IRR' }
39
+ it 'includes the result of the calculation' do
40
+ expect(subject).to include irr.result.to_s
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Npv" do
4
+ let(:rate) { 0.1 }
5
+ let(:cashflows) { [10] * 4 }
6
+ let(:npv) { Npv.new(rate, cashflows) }
7
+
8
+ subject { Npv.new(rate, cashflows) }
9
+
10
+ it 'has a rate attribute' do
11
+ expect(subject).to have_attributes(rate: rate)
12
+ end
13
+
14
+ it 'has an cashflows attribute' do
15
+ expect(subject).to have_attributes(cashflows: cashflows)
16
+ end
17
+
18
+ it 'has a result attribute' do
19
+ expect(subject.result).to be_a Numeric
20
+ end
21
+
22
+ context 'when provided a non-numeric rate' do
23
+ let(:rate) { 'string' }
24
+
25
+ it 'raises an ArgumentError' do
26
+ expect { subject }.to raise_error ArgumentError
27
+ end
28
+ end
29
+
30
+ describe '#inspect' do
31
+ subject { npv.inspect }
32
+
33
+ it { is_expected.to be_a String }
34
+ it { is_expected.to include 'NPV' }
35
+ it 'includes the result of the Npv calculation' do
36
+ expect(subject).to include npv.result.to_s
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,127 @@
1
+ require_relative 'spec_helper'
2
+
3
+ shared_examples_for 'it has invalid present value arguments' do
4
+ it 'raises an ArgumentError' do
5
+ expect { subject }.to raise_error ArgumentError
6
+ end
7
+ end
8
+
9
+ describe "Pv" do
10
+ let(:rate) { 0.1 }
11
+ let(:num_periods) { 10 }
12
+ let(:payment) { -100 }
13
+ let(:future_value) { 0 }
14
+ let(:pays_at_beginning) { false }
15
+ let(:present_value) { Pv.new(rate, num_periods, payment) }
16
+
17
+ subject { present_value }
18
+
19
+ it 'has a rate attribute' do
20
+ expect(subject).to have_attributes(rate: rate)
21
+ end
22
+
23
+ it 'has a num_periods attribute' do
24
+ expect(subject).to have_attributes(num_periods: num_periods)
25
+ end
26
+
27
+ it 'has a payment attribute' do
28
+ expect(subject).to have_attributes(payment: payment)
29
+ end
30
+
31
+ it 'has a future_value attribute that defaults to 0' do
32
+ expect(subject).to have_attributes(future_value: 0)
33
+ end
34
+
35
+ it 'has a pays_at_beginning? attribute that defaults to false' do
36
+ expect(subject).to have_attributes(pays_at_beginning?: false)
37
+ end
38
+
39
+ it 'has a result attribute whose value is positive' do
40
+ expect(subject).to have_attributes(result: (a_value > 0))
41
+ end
42
+
43
+ context 'when the payment is positive' do
44
+ let(:payment) { 100 }
45
+
46
+ it 'has a result attribute whose value is negative' do
47
+ expect(subject).to have_attributes(result: (a_value < 0))
48
+ end
49
+ end
50
+
51
+ context 'when the payment is 0' do
52
+ let(:payment) { 0 }
53
+
54
+ it 'has a present value of 0' do
55
+ expect(subject.result).to eql 0
56
+ end
57
+ end
58
+
59
+ context 'when the number of periods is negative' do
60
+ let(:num_periods) { -1 }
61
+ it_behaves_like 'it has invalid present value arguments'
62
+ end
63
+
64
+ context 'when rate is non-numeric' do
65
+ let(:rate) { 'string' }
66
+ it_behaves_like 'it has invalid present value arguments'
67
+ end
68
+
69
+ context 'when num_periods is non-numeric' do
70
+ let(:num_periods) { 'string' }
71
+ it_behaves_like 'it has invalid present value arguments'
72
+ end
73
+
74
+ context 'when payment is non-numeric' do
75
+ let(:payment) { 'string' }
76
+ it_behaves_like 'it has invalid present value arguments'
77
+ end
78
+
79
+ context 'when future_values non-numeric' do
80
+ let(:future_value) { 'string' }
81
+
82
+ subject { Pv.new(rate, num_periods, payment, future_value) }
83
+
84
+ it_behaves_like 'it has invalid present value arguments'
85
+ end
86
+
87
+ context 'when the number of periods is 0' do
88
+ let(:num_periods) { 0 }
89
+
90
+ it 'has a result equal to the future value' do
91
+ expect(subject.result).to eql future_value
92
+ end
93
+ end
94
+
95
+ context 'when provided with a future value' do
96
+ let(:future_value) { 100 }
97
+
98
+ subject { Pv.new(rate, num_periods, payment, future_value) }
99
+
100
+ it { is_expected.to have_attributes(future_value: future_value) }
101
+ end
102
+
103
+ context 'when payments occur at the beginning of each period' do
104
+ let(:pays_at_beginning) { true }
105
+
106
+ subject { Pv.new(rate, num_periods, payment, 0, pays_at_beginning) }
107
+
108
+ it { is_expected.to have_attributes(pays_at_beginning?: true) }
109
+ end
110
+
111
+ describe '#inspect' do
112
+
113
+ subject { present_value.inspect }
114
+
115
+ it { is_expected.to be_a String }
116
+ it { is_expected.to include 'PV' }
117
+ it 'includes the result of the present value calculation' do
118
+ expect(subject).to include present_value.result.to_s
119
+ end
120
+ end
121
+
122
+ describe '#result' do
123
+ subject { present_value.result }
124
+
125
+ it {is_expected.to be_a Flt::DecNum }
126
+ end
127
+ end
@@ -1,14 +1,12 @@
1
1
  require 'simplecov'
2
2
  require 'coveralls'
3
3
 
4
- Coveralls.wear!
4
+ # Coveralls.wear!
5
5
  SimpleCov.start do
6
- minimum_coverage 99
6
+ add_filter "/spec/"
7
+ minimum_coverage 100
7
8
  end
8
9
 
9
- require 'minitest/autorun'
10
- require 'minitest/spec'
11
-
12
10
  require 'active_support/all'
13
11
 
14
12
  require 'pry'
@@ -16,8 +14,11 @@ require 'pry'
16
14
  require 'flt'
17
15
  require 'flt/d'
18
16
 
19
- require_relative '../lib/financial_calculator/amortization.rb'
20
- require_relative '../lib/financial_calculator/cashflows.rb'
21
- require_relative '../lib/financial_calculator/rates.rb'
22
- require_relative '../lib/financial_calculator/transaction.rb'
17
+ require_relative '../lib/financial_calculator'
23
18
  include FinancialCalculator
19
+
20
+ shared_examples_for 'the values do not converge' do
21
+ it 'should raise an ArgumentError' do
22
+ expect { subject }.to raise_error ArgumentError
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Xirr" do
4
+ let(:flows) { [-10, 20, 30, 40] }
5
+ let(:dates) do
6
+ [
7
+ Date.new(2018, 1, 1),
8
+ Date.new(2018, 6, 1),
9
+ Date.new(2018, 9, 1),
10
+ Date.new(2018, 12, 1)
11
+ ]
12
+ end
13
+
14
+ describe '::with_transactions' do
15
+ let(:transactions) do
16
+ flows.zip(dates).map { |flow_and_date| Transaction.new(flow_and_date[0], date: flow_and_date[1]) }
17
+ end
18
+
19
+ subject { Xirr.with_transactions(transactions) }
20
+
21
+ it 'returns an Xirr object' do
22
+ expect(subject).to be_a Xirr
23
+ end
24
+
25
+ context 'the array contains objects other than Transaction' do
26
+ let(:transactions) { flows }
27
+
28
+ it 'raises an ArgumentError' do
29
+ expect { subject }.to raise_error ArgumentError
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#result' do
35
+ subject { Xirr.new(flows, dates).result }
36
+
37
+ it { is_expected.to be_a Flt::DecNum }
38
+
39
+ context 'when all values are positive' do
40
+ let(:flows) { [10, 20, 30, 40] }
41
+ let(:flows) { [10, 20, 30, 40] }
42
+ it_behaves_like 'the values do not converge'
43
+ end
44
+
45
+ context 'when all values are negative' do
46
+ let(:flows) { [-10, -20, -30, -40] }
47
+ it_behaves_like 'the values do not converge'
48
+ end
49
+
50
+ context 'when the leading value is zero' do
51
+ describe 'all other values are positive' do
52
+ let(:flows) { [0, 10, 20, 30] }
53
+ it_behaves_like 'the values do not converge'
54
+ end
55
+
56
+ describe 'all other values are negative' do
57
+ let(:flows) { [0, -10, -20, -30] }
58
+ it_behaves_like 'the values do not converge'
59
+ end
60
+ end
61
+ end
62
+
63
+ describe '#inspect' do
64
+ let(:xirr) { Xirr.new(flows, dates) }
65
+ subject { xirr.inspect }
66
+
67
+ it { is_expected.to be_a String }
68
+ it { is_expected.to include 'XIRR' }
69
+ it 'includes the result of the calculation' do
70
+ expect(subject).to include xirr.result.to_s
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,81 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Xnpv" do
4
+ let(:cashflows) { [1000] * 5 }
5
+ let(:year) { 2017 }
6
+ let(:rate) { 0.02 }
7
+ let(:xnpv) { Xnpv.new(rate, cashflows, dates) }
8
+ let(:dates) do
9
+ [
10
+ Date.new(year, 1, 1),
11
+ Date.new(year, 3, 1),
12
+ Date.new(year, 6, 1),
13
+ Date.new(year, 6, 15),
14
+ Date.new(year, 6, 30)
15
+ ]
16
+ end
17
+
18
+ subject { xnpv }
19
+
20
+ it 'has a cashflows property' do
21
+ expect(subject).to have_attributes(cashflows: cashflows)
22
+ end
23
+
24
+ it 'has a rate property' do
25
+ expect(subject).to have_attributes(rate: rate)
26
+ end
27
+
28
+ it 'has a dates property' do
29
+ expect(subject).to have_attributes(dates: dates)
30
+ end
31
+
32
+ it 'has a result property' do
33
+ expect(subject.result).to be_a Flt::DecNum
34
+ end
35
+
36
+ context 'when dates and cashflows are not the same size' do
37
+ let(:transactions) { cashflows[1..-1] }
38
+
39
+ subject { Xnpv.new(rate, transactions, dates) }
40
+
41
+ it 'raises an ArgumentError' do
42
+ expect { subject }.to raise_error ArgumentError
43
+ end
44
+ end
45
+
46
+ context 'when the dates array does not contain only dates' do
47
+ let(:dates) { [Date.today] * 4 + [1] }
48
+
49
+ it 'raises an ArgumentError' do
50
+ expect { subject }.to raise_error ArgumentError
51
+ end
52
+ end
53
+
54
+ describe '::with_transactions' do
55
+ let(:transactions) { dates.map { |date| Transaction.new(1000, date: date) } }
56
+
57
+ subject { Xnpv.with_transactions(rate, transactions) }
58
+
59
+ it 'returns an Xnpv object' do
60
+ expect(subject).to be_a Xnpv
61
+ end
62
+
63
+ context 'the array contains objects other than Transaction' do
64
+ let(:transactions) { cashflows }
65
+
66
+ it 'raises an ArgumentError' do
67
+ expect { subject }.to raise_error ArgumentError
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#inspect' do
73
+ subject { xnpv.inspect }
74
+
75
+ it { is_expected.to include 'XNPV' }
76
+
77
+ it 'includes the result of the calculation' do
78
+ expect(subject).to include xnpv.result.to_s
79
+ end
80
+ end
81
+ end