financial_calculator 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ require_relative 'spec_helper'
2
+
3
+ # @see http://tinyurl.com/6zroqvd for detailed calculations for the
4
+ # examples in these unit tests.
5
+ describe "Amortization" do
6
+ def ipmt(principal, rate, payment, period)
7
+ -(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
8
+ end
9
+
10
+ describe '#inspect' do
11
+ let(:amount) { '10000.00' }
12
+ subject { Amortization.new(D(amount), Rate.new(0.0375, :apr, duration: (30 * 12))).inspect }
13
+
14
+ it { is_expected.to be_a String }
15
+ it { is_expected.to include "Amortization" }
16
+ it { is_expected.to include amount }
17
+ end
18
+
19
+ describe "amortization with a 0% rate" do
20
+ let(:rate) { Rate.new(0, :apr, :duration => 30 * 12) }
21
+ subject { Amortization.new(D(10000), rate) }
22
+
23
+ it 'should not raise a divide-by-zero error' do
24
+ expect { subject }.to_not raise_error
25
+ end
26
+ end
27
+
28
+ describe "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
29
+ before(:all) do
30
+ @rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
31
+ @principal = D(200000)
32
+ @std = Amortization.new(@principal, @rate)
33
+ end
34
+
35
+ it "should have a principal of $200,000" do
36
+ expect(@principal).to eql @std.principal
37
+ end
38
+
39
+ it "should have a final balance of zero" do
40
+ expect(@std.balance).to eql 0
41
+ end
42
+
43
+ it "should have a duration of 360 months" do
44
+ expect(@std.duration).to eql 360
45
+ end
46
+
47
+ it "should have a monthly payment of $926.23" do
48
+ expect(@std.payment).to eql D('-926.23')
49
+ end
50
+
51
+ it "should have a final payment of $926.96 (due to rounding)" do
52
+ expect(@std.payments[-1]).to eql D('-926.96')
53
+ end
54
+
55
+ it "should have total payments of $333,443.53" do
56
+ expect(@std.payments.sum).to eql D('-333443.53')
57
+ end
58
+
59
+ it "should have interest charges which agree with the standard formula" do
60
+ 0.upto 359 do |period|
61
+ expect(@std.interest[period]).to eql ipmt(@principal, @rate.monthly, @std.payment, period+1)
62
+ end
63
+ end
64
+
65
+ it "should have total interest charges of $133,443.33" do
66
+ expect(@std.interest.sum).to eql D('133443.53')
67
+ end
68
+ end
69
+
70
+ describe "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
71
+ before(:all) do
72
+ @rates = []
73
+ 0.upto 9 do |adj|
74
+ @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => (3 * 12))
75
+ end
76
+ @principal = D(200000)
77
+ @arm = Amortization.new(@principal, *@rates)
78
+ end
79
+
80
+ it "should have a principal of $200,000" do
81
+ expect(@principal).to eql @arm.principal
82
+ end
83
+
84
+ it "should have a final balance of zero" do
85
+ expect(@arm.balance).to eql 0
86
+ end
87
+
88
+ it "should have a duration of 360 months" do
89
+ expect(@arm.duration).to eql 360
90
+ end
91
+
92
+ it "should not have a fixed monthly payment (since it changes)" do
93
+ expect(@arm.payment).to be nil
94
+ end
95
+
96
+ it "should have payments which increase every three years" do
97
+ values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 }
98
+ values.collect!{ |v| -D(v) }
99
+
100
+ payments = []
101
+ values[0,9].each do |v|
102
+ 36.times do
103
+ payments << v
104
+ end
105
+ end
106
+
107
+ 35.times { payments << values[9] }
108
+
109
+ payments[0..-2].each_with_index do |payment, index|
110
+ expect(payment).to eql @arm.payments[index]
111
+ end
112
+ end
113
+
114
+ it "should have a final payment of $1601.78 (due to rounding)" do
115
+ expect(@arm.payments[-1]).to eql D('-1601.78')
116
+ end
117
+
118
+ it "should have total payments of $47,505.92" do
119
+ expect(@arm.payments.sum).to eql D('-477505.92')
120
+ end
121
+
122
+ it "should have total interest charges of $277,505.92" do
123
+ expect(@arm.interest.sum).to eql D('277505.92')
124
+ end
125
+ end
126
+
127
+ describe "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
128
+ before(:all) do
129
+ @rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
130
+ @principal = D(200000)
131
+ @exp = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
132
+ end
133
+
134
+ it "should have a principal of $200,000" do
135
+ expect(@exp.principal).to eql @principal
136
+ end
137
+
138
+ it "should have a final balance of zero" do
139
+ expect(@exp.balance).to eql D('0.00')
140
+ end
141
+
142
+ it "should have a duration of 301 months" do
143
+ expect(@exp.duration).to eql 301
144
+ end
145
+
146
+ it "should have a monthly payment of $1026.23" do
147
+ expect(@exp.payment).to eql D('-1026.23')
148
+ end
149
+
150
+ it "should have a final payment of $1011.09" do
151
+ expect(@exp.payments[-1]).to eql D('-1011.09')
152
+ end
153
+
154
+ it "should have total payments of $308,880.09" do
155
+ expect(@exp.payments.sum).to eql D('-308880.09')
156
+ end
157
+
158
+ it "should have total additional payments of $30,084.86" do
159
+ expect(@exp.additional_payments.sum).to eql D('-30084.86')
160
+ end
161
+
162
+ it "should have total interest charges of $108880.09" do
163
+ expect(@exp.interest.sum).to eql D('108880.09')
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "Numeric Method" do
169
+ it 'works with simple invocation' do
170
+ rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
171
+ amt_method = 300000.amortize(rate)
172
+ amt_class = Amortization.new(300000, rate)
173
+ expect(amt_method).to eq amt_class
174
+ end
175
+
176
+ it 'works with block invocation' do
177
+ rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
178
+ amt_method = 300000.amortize(rate){ |period| period.payment-300 }
179
+ amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
180
+ expect(amt_method).to eq amt_class
181
+ end
182
+ end
@@ -0,0 +1,66 @@
1
+ require_relative 'spec_helper'
2
+
3
+ shared_examples_for 'the values do not converge' do
4
+ it 'should raise an ArgumentError' do
5
+ expect { subject }.to raise_error ArgumentError
6
+ end
7
+ end
8
+
9
+ describe "Cashflows" do
10
+ let(:transactions) do
11
+ [
12
+ Transaction.new(-1000, :date => Time.new(1985, 1, 1)),
13
+ Transaction.new( 600, :date => Time.new(1990, 1, 1)),
14
+ Transaction.new( 600, :date => Time.new(1995, 1, 1))
15
+ ]
16
+ end
17
+
18
+ describe '#irr' do
19
+ let(:flows) { [-4000, 1200, 1410, 1875, 1050] }
20
+
21
+ subject { flows.irr.round(3) }
22
+
23
+ it { is_expected.to eql D('0.143') }
24
+
25
+ context 'when called on an array of Transactions' do
26
+ let(:flows) { transactions }
27
+
28
+ it 'should raise a NoMethodError' do
29
+ expect { subject }.to raise_error NoMethodError
30
+ end
31
+ end
32
+
33
+ context 'when the values do not converge' do
34
+ let(:flows) { [10, 20, 30] }
35
+ it_behaves_like 'the values do not converge'
36
+ end
37
+ end
38
+
39
+ describe '#npv' do
40
+ let(:flows) { [-100.0, 60, 60, 60] }
41
+
42
+ subject { flows.npv(0.1).round(3) }
43
+
44
+ it { is_expected.to eql D('49.211') }
45
+ end
46
+
47
+ describe '#xirr' do
48
+ let(:xirr_transactions) { transactions }
49
+
50
+ subject { xirr_transactions.xirr.effective.round(6) }
51
+
52
+ it { is_expected.to eql D('0.024851') }
53
+
54
+ context 'when the values do not converge' do
55
+ let(:xirr_transactions) { transactions[1,3] }
56
+
57
+ it_behaves_like 'the values do not converge'
58
+ end
59
+ end
60
+
61
+ describe '#xnpv' do
62
+ subject { transactions.xnpv(0.6).round(2) }
63
+
64
+ it { is_expected.to eql -937.41 }
65
+ end
66
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'spec_helper'
2
+ require_relative '../lib/financial_calculator/decimal.rb'
3
+
4
+ describe "Numeric" do
5
+ let(:dec_num) { D('12.123') }
6
+
7
+ subject { dec_num }
8
+
9
+ describe 'it converts to a BigDecimal' do
10
+ subject { dec_num.convert_to BigDecimal }
11
+
12
+ it { is_expected.to be_a BigDecimal }
13
+ end
14
+
15
+ describe '#to_d' do
16
+ context 'when already a Flt::DecNum' do
17
+ let(:dec_num) { D('12.123') }
18
+
19
+ subject { dec_num.to_d }
20
+
21
+ it { is_expected.to be_a Flt::DecNum }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,188 @@
1
+ require_relative 'spec_helper'
2
+
3
+ shared_examples_for 'does not raise error' do
4
+ it 'does not raise an error' do
5
+ expect { subject }.to_not raise_error
6
+ end
7
+ end
8
+
9
+ describe "Rates" do
10
+ let(:test_rate) { 0.15 }
11
+ let(:compounding_Period) { :monthly }
12
+ let(:type) { :nominal }
13
+ let(:opts) { nil }
14
+ let(:rate) { Rate.new(test_rate, type) }
15
+
16
+ subject { rate }
17
+
18
+ context 'when given a duration' do
19
+ let(:type) { :effective }
20
+ let(:rate) { Rate.new(test_rate, type, duration: 360) }
21
+
22
+ subject { rate.duration }
23
+
24
+ it { is_expected.to eql 360 }
25
+ end
26
+
27
+ describe 'when compared to another interest rate' do
28
+ let(:r1) { rate }
29
+ let(:r2) { Rate.new(compared_rate, type) }
30
+
31
+ subject { r1 <=> r2 }
32
+
33
+ context 'when the other rate is smaller' do
34
+ let(:compared_rate) { test_rate - 0.01}
35
+ it { is_expected.to eql 1 }
36
+ end
37
+
38
+ context 'when the other rate is the same' do
39
+ let(:compared_rate) { test_rate }
40
+ it { is_expected.to eql 0 }
41
+ end
42
+
43
+ context 'when the other rate is bigger' do
44
+ let(:compared_rate) { test_rate + 0.01 }
45
+ it { is_expected.to eql -1 }
46
+
47
+ end
48
+ end
49
+
50
+ describe "should convert to a monthly value" do
51
+ let(:type) { :effective }
52
+ let(:test_rate) { 0.0375 }
53
+
54
+ subject { rate.monthly }
55
+
56
+ it { is_expected.to eql D('0.003125') }
57
+ end
58
+
59
+ describe 'converts effective interest rates to nominal' do
60
+ context 'when the rates compounding period is finite' do
61
+ subject { Rate.to_nominal(D('0.0375'), 12).round(5) }
62
+
63
+ it { is_expected.to eql D('0.03687') }
64
+ end
65
+
66
+ context 'when the rate is continuously compounding' do
67
+ subject { Rate.to_nominal(D('0.0375'), Flt::DecNum.infinity).round(5) }
68
+
69
+ it { is_expected.to eql D('0.03681') }
70
+ end
71
+ end
72
+
73
+ context 'type is an unknown value' do
74
+ let(:type) { :foo }
75
+
76
+ it 'should raise an ArgumentError' do
77
+ expect { subject }.to raise_error ArgumentError
78
+ end
79
+ end
80
+
81
+ context 'with compounding period' do
82
+ let(:rate) { Rate.new(test_rate, type, compounds: compounding_period) }
83
+
84
+ describe 'anually' do
85
+ let(:compounding_period) { :annually }
86
+ it_behaves_like 'does not raise error'
87
+ end
88
+
89
+ describe 'continuously' do
90
+ let(:compounding_period) { :continuously }
91
+ it_behaves_like 'does not raise error'
92
+ end
93
+
94
+ describe 'daily' do
95
+ let(:compounding_period) { :daily }
96
+ it_behaves_like 'does not raise error'
97
+ end
98
+
99
+ describe 'monthly' do
100
+ let(:compounding_period) { :monthly }
101
+ it_behaves_like 'does not raise error'
102
+ end
103
+
104
+ describe 'quarterly' do
105
+ let(:compounding_period) { :quarterly }
106
+ it_behaves_like 'does not raise error'
107
+ end
108
+
109
+ describe 'semiannually' do
110
+ let(:compounding_period) { :semiannually }
111
+ it_behaves_like 'does not raise error'
112
+ end
113
+
114
+ describe 'Numeric' do
115
+ let(:compounding_period) { 7 }
116
+ it_behaves_like 'does not raise error'
117
+ end
118
+
119
+ describe 'unknown string' do
120
+ let(:compounding_period) { :foo }
121
+ it 'raises an error' do
122
+ expect { subject }.to raise_error ArgumentError
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#inspect' do
128
+ subject { rate.inspect }
129
+
130
+ it { is_expected.to be_a String }
131
+ it { is_expected.to include ':apr' }
132
+ end
133
+
134
+ describe '#apr' do
135
+ subject { rate.apr }
136
+
137
+ it { is_expected.to eql rate.effective }
138
+ end
139
+
140
+ describe '#apy' do
141
+ subject { rate.apy }
142
+
143
+ it { is_expected.to eql rate.effective }
144
+ end
145
+
146
+ describe "#effective" do
147
+ context 'with compounding period' do
148
+ let(:rate) { Rate.new(test_rate, :nominal, compounds: compounding_period) }
149
+
150
+ subject { rate.effective.round(5) }
151
+
152
+ describe 'monthly' do
153
+ let(:compounding_period) { :monthly }
154
+ it { should == D('0.16075') }
155
+ end
156
+
157
+ describe 'annually' do
158
+ let(:compounding_period) { :annually }
159
+ it { should == D('0.15000') }
160
+ end
161
+
162
+ describe 'continuously' do
163
+ let(:compounding_period) { :continuously }
164
+ it { should == D('0.16183') }
165
+ end
166
+
167
+ describe 'daily' do
168
+ let(:compounding_period) { :daily }
169
+ it { should == D('0.16180') }
170
+ end
171
+
172
+ describe 'quarterly' do
173
+ let(:compounding_period) { :quarterly }
174
+ it { should == D('0.15865') }
175
+ end
176
+
177
+ describe 'semi-annually' do
178
+ let(:compounding_period) { :semiannually }
179
+ it { should == D('0.15563') }
180
+ end
181
+
182
+ describe 'Numeric' do
183
+ let(:compounding_period) { 7 }
184
+ it { should == D('0.15999') }
185
+ end
186
+ end
187
+ end
188
+ end