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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rbenv-version +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/COPYING +676 -0
- data/COPYING.LESSER +167 -0
- data/Gemfile +3 -0
- data/HISTORY +47 -0
- data/README.md +168 -0
- data/Rakefile +10 -0
- data/financial_calculator.gemspec +34 -0
- data/lib/financial_calculator/amortization.rb +201 -0
- data/lib/financial_calculator/cashflows.rb +127 -0
- data/lib/financial_calculator/decimal.rb +21 -0
- data/lib/financial_calculator/rates.rb +167 -0
- data/lib/financial_calculator/transaction.rb +122 -0
- data/lib/financial_calculator/version.rb +3 -0
- data/lib/financial_calculator.rb +15 -0
- data/spec/amortization_spec.rb +182 -0
- data/spec/cashflows_spec.rb +66 -0
- data/spec/decimal_spec.rb +24 -0
- data/spec/rates_spec.rb +188 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/transaction/interest_spec.rb +14 -0
- data/spec/transaction/payment_spec.rb +14 -0
- data/spec/transaction_spec.rb +14 -0
- metadata +194 -0
@@ -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
|
data/spec/rates_spec.rb
ADDED
@@ -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
|