finance 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +72 -9
- data/lib/amortization.rb +147 -78
- data/lib/cashflows.rb +25 -25
- data/lib/finance.rb +48 -48
- data/lib/rates.rb +83 -79
- data/lib/timespan.rb +7 -7
- data/test/test_amortization.rb +164 -31
- data/test/test_cashflows.rb +6 -6
- data/test/test_rates.rb +21 -21
- data/test/test_timespan.rb +6 -6
- metadata +5 -5
data/README
CHANGED
@@ -12,7 +12,8 @@ _finance_ - a library for financial calculations in Ruby.
|
|
12
12
|
|
13
13
|
== AMORTIZATION
|
14
14
|
|
15
|
-
You are interested in borrowing $250,000 under a 30 year, fixed-rate
|
15
|
+
You are interested in borrowing $250,000 under a 30 year, fixed-rate
|
16
|
+
loan with a 4.25% APR.
|
16
17
|
|
17
18
|
>> rate = Rate.new(0.0425, :apr, :duration => 30.years)
|
18
19
|
>> amortization = 250000.amortize(rate)
|
@@ -37,11 +38,74 @@ How much interest in the first six months?
|
|
37
38
|
>> amortization.interest[0,6].sum
|
38
39
|
=> DecNum('5294.62')
|
39
40
|
|
41
|
+
If your loan has an adjustable rate, no problem. You can pass an
|
42
|
+
arbitrary number of rates, and they will be used in the amortization.
|
43
|
+
For example, we can look at an amortization of $250000, where the APR
|
44
|
+
starts at 4.25%, and increases by 1% every five years.
|
45
|
+
|
46
|
+
>> values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
|
47
|
+
>> rates = values.collect { |value| Rate.new( value, :apr, :duration = 5.years ) }
|
48
|
+
>> arm = Amortization.new(250000, *rates)
|
49
|
+
|
50
|
+
Since we are looking at an ARM, there is no longer a single "payment" value.
|
51
|
+
|
52
|
+
>> arm.payment
|
53
|
+
=> nil
|
54
|
+
|
55
|
+
But we can look at the different payments over time.
|
56
|
+
|
57
|
+
>> arm.payments.uniq
|
58
|
+
=> [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), DecNum('-1641.34'), ... snipped ... ]
|
59
|
+
|
60
|
+
The other methods previously discussed can be accessed in the same way:
|
61
|
+
|
62
|
+
>> arm.interest.sum
|
63
|
+
=> DecNum('287515.45')
|
64
|
+
>> arm.payments.sum
|
65
|
+
=> DecNum('-537515.45')
|
66
|
+
|
67
|
+
Last, but not least, you may pass a block when creating an Amortization
|
68
|
+
which returns a modified monthly payment. For example, to increase your
|
69
|
+
payment by $150, do:
|
70
|
+
|
71
|
+
>> rate = Rate.new(0.0425, :apr, :duration => 30.years)
|
72
|
+
>> extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
|
73
|
+
|
74
|
+
Disregarding the block, we have used the same parameters as the first
|
75
|
+
example. Notice the difference in the results:
|
76
|
+
|
77
|
+
>> amortization.payments.sum
|
78
|
+
=> DecNum('-442745.98')
|
79
|
+
>> extra_payments.payments.sum
|
80
|
+
=> DecNum('-400566.24')
|
81
|
+
>> amortization.interest.sum
|
82
|
+
=> DecNum('192745.98')
|
83
|
+
>> extra_payments.interest.sum
|
84
|
+
=> DecNum('150566.24')
|
85
|
+
|
86
|
+
*Note*: you are _not_ allowed to modify a payment to pay _less_ than the
|
87
|
+
normally calculated payment.
|
88
|
+
|
89
|
+
You can also increase your payment to a specific amount:
|
90
|
+
|
91
|
+
>> extra_payments_2 = 250000.amortize(rate){ -1500 }
|
92
|
+
|
40
93
|
= ABOUT
|
41
94
|
|
42
|
-
I
|
43
|
-
|
44
|
-
|
95
|
+
I started developing _finance_ while analyzing mortgages as a personal
|
96
|
+
project. Spreadsheets have convenient formulas for doing this type of
|
97
|
+
work, until you want to do something semi-complex (like ARMs or extra
|
98
|
+
payments), at which point you need to create your own amortization
|
99
|
+
table. I thought I could create a better interface for this type of
|
100
|
+
work in Ruby, and since I couldn't find an existing resource for these
|
101
|
+
tools, I am hoping to save other folks some time by releasing what I
|
102
|
+
have as a gem.
|
103
|
+
|
104
|
+
More broadly, I believe there are many calculations that are necessary
|
105
|
+
for the effective management of personal finances, but are difficult
|
106
|
+
(or impossible) to do with spreadsheets or other existing open source
|
107
|
+
tools. My hope is that the _finance_ library will grow to provide a set
|
108
|
+
of open, tested tools to fill this gap.
|
45
109
|
|
46
110
|
If you have used _finance_ and find it useful, I would enjoy hearing
|
47
111
|
about it!
|
@@ -50,16 +114,12 @@ about it!
|
|
50
114
|
|
51
115
|
Currently implemented features include:
|
52
116
|
|
117
|
+
* Uses the {http://flt.rubyforge.org/ flt} library to ensure precision decimal arithmetic in all calculations.
|
53
118
|
* Fixed-rate mortgage amortization (30/360).
|
54
119
|
* Interest rates
|
55
120
|
* Various cash flow computations, such as NPV, IRR, and sum.
|
56
|
-
|
57
|
-
Planned features include:
|
58
|
-
|
59
121
|
* Adjustable rate mortgage amortization.
|
60
122
|
* Payment modifications (i.e., how does paying an additional $75 per month affect the amortization?)
|
61
|
-
* Balloon payments.
|
62
|
-
* Support for amortization methods other than 30/360.
|
63
123
|
|
64
124
|
= RESOURCES
|
65
125
|
|
@@ -69,6 +129,9 @@ This gem and related documentation is available through
|
|
69
129
|
Source code and bug tracking is available via
|
70
130
|
{http://github.com/wkranec/finance github}.
|
71
131
|
|
132
|
+
Additional documentation is available on the
|
133
|
+
{https://github.com/wkranec/finance/wiki wiki}.
|
134
|
+
|
72
135
|
= COPYRIGHT
|
73
136
|
|
74
137
|
This library is released under the terms of the LGPL license.
|
data/lib/amortization.rb
CHANGED
@@ -1,91 +1,160 @@
|
|
1
|
+
require 'rubygems'
|
1
2
|
require 'cashflows'
|
3
|
+
require 'flt'
|
2
4
|
|
3
5
|
class Period
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
6
|
+
attr_accessor :payment
|
7
|
+
attr_accessor :principal
|
8
|
+
attr_accessor :rate
|
9
|
+
|
10
|
+
def additional_payment
|
11
|
+
@payment-@original_payment
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return the remaining balance at the end of the period.
|
15
|
+
def balance
|
16
|
+
@principal + @payment + self.interest
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(principal, rate, payment)
|
20
|
+
@principal = principal
|
21
|
+
@rate = rate
|
22
|
+
@payment = payment
|
23
|
+
@original_payment = payment
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the interest charged for the period.
|
27
|
+
def interest
|
28
|
+
(@principal * @rate).round(2)
|
29
|
+
end
|
30
|
+
|
31
|
+
def modify_payment(&modifier)
|
32
|
+
value = modifier.call(self)
|
33
|
+
|
34
|
+
# There's a chance that the block does not return a decimal.
|
35
|
+
unless value.class == Flt::DecNum
|
36
|
+
value = Flt::DecNum value.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
self.payment = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def payment=(value)
|
43
|
+
total = @principal + self.interest
|
44
|
+
if value.abs > total
|
45
|
+
@payment = -total
|
46
|
+
else
|
47
|
+
@payment = value
|
48
|
+
end
|
49
|
+
end
|
23
50
|
end
|
24
51
|
|
25
52
|
class Amortization
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
53
|
+
attr_accessor :balance
|
54
|
+
attr_accessor :block
|
55
|
+
attr_accessor :rate_duration
|
56
|
+
attr_accessor :payment
|
57
|
+
attr_accessor :periods
|
58
|
+
attr_accessor :principal
|
59
|
+
attr_accessor :rates
|
60
|
+
|
61
|
+
def ==(amortization)
|
62
|
+
self.principal == amortization.principal and self.rates == amortization.rates and self.payments == amortization.payments
|
63
|
+
end
|
64
|
+
|
65
|
+
def additional_payments
|
66
|
+
@periods.collect{ |period| period.additional_payment }
|
67
|
+
end
|
68
|
+
|
69
|
+
def amortize(rate)
|
70
|
+
# For the purposes of calculating a payment, the relevant time
|
71
|
+
# period is the remaining number of periods in the loan, _not_ the
|
72
|
+
# duration of the rate itself.
|
73
|
+
duration = @rate_duration - @periods.length
|
74
|
+
payment = Amortization.payment @balance, rate.monthly, duration
|
75
|
+
|
76
|
+
rate.duration.times do
|
77
|
+
# Do this first in case the balance is zero already.
|
78
|
+
if @balance.zero?
|
79
|
+
break
|
80
|
+
end
|
81
|
+
|
82
|
+
period = Period.new(@balance, rate.monthly, payment)
|
83
|
+
|
84
|
+
if @block
|
85
|
+
period.modify_payment(&@block)
|
86
|
+
end
|
87
|
+
|
88
|
+
@periods << period
|
89
|
+
@balance = period.balance
|
90
|
+
end
|
91
|
+
|
92
|
+
if @rates.length == 1
|
93
|
+
@payment = self.payments[0]
|
94
|
+
else
|
95
|
+
@payment = nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Compute the amortization of the principal.
|
100
|
+
def compute
|
101
|
+
@balance = @principal
|
102
|
+
@periods = []
|
103
|
+
|
104
|
+
@rates.each do |rate|
|
105
|
+
amortize(rate)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Add any remaining balance due to rounding error to the last payment.
|
109
|
+
unless @balance.zero?
|
110
|
+
@periods[-1].payment -= @balance
|
111
|
+
@balance = 0
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def duration
|
116
|
+
@periods.length
|
117
|
+
end
|
118
|
+
|
119
|
+
def initialize(principal, *rates, &block)
|
120
|
+
@principal = principal
|
121
|
+
@rates = rates
|
122
|
+
@rate_duration = (rates.collect { |r| r.duration }).sum
|
123
|
+
@block = block
|
124
|
+
|
125
|
+
compute
|
126
|
+
end
|
127
|
+
|
128
|
+
def inspect
|
129
|
+
"Amortization.new(#{@principal}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return an Array with the amount of interest charged in each period.
|
133
|
+
def interest
|
134
|
+
@periods.collect { |period| period.interest }
|
135
|
+
end
|
75
136
|
|
76
137
|
# Return the periodic payment due on a loan, based on the
|
77
138
|
#{http://en.wikipedia.org/wiki/Amortization_calculator amortization process}.
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
139
|
+
def Amortization.payment(balance, rate, periods)
|
140
|
+
-(balance * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Return an array with the payment amount for each period.
|
144
|
+
def payments
|
145
|
+
@periods.collect { |period| period.payment }
|
146
|
+
end
|
147
|
+
|
148
|
+
# "Pretty print" a text amortization table.
|
149
|
+
def pprint
|
150
|
+
@periods.each_with_index do |p, i|
|
151
|
+
puts "%03d $%9s %8s $%7s $%7s $%9s" % [i, p.principal, p.rate, p.payment, p.interest, p.balance]
|
152
|
+
end
|
153
|
+
end
|
85
154
|
end
|
86
155
|
|
87
156
|
class Numeric
|
88
|
-
|
89
|
-
|
90
|
-
|
157
|
+
def amortize(rate, &block)
|
158
|
+
amortization = Amortization.new(self, rate, &block)
|
159
|
+
end
|
91
160
|
end
|
data/lib/cashflows.rb
CHANGED
@@ -1,33 +1,33 @@
|
|
1
1
|
module Cashflow
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
2
|
+
# Return the {http://en.wikipedia.org/wiki/Internal_rate_of_return
|
3
|
+
# internal rate of return} for a given sequence of cashflows.
|
4
|
+
def irr(iterations=100)
|
5
|
+
rate = 1.0
|
6
|
+
investment = self[0]
|
7
|
+
for i in 1..iterations+1
|
8
|
+
rate = rate * (1 - self.npv(rate) / investment)
|
9
|
+
end
|
10
|
+
rate
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
# Return the {http://en.wikipedia.org/wiki/Net_present_value net present value} of a sequence of cash flows given
|
14
|
+
# the discount rate _rate_.
|
15
|
+
def npv(rate)
|
16
|
+
total = 0.0
|
17
|
+
self.each_with_index do |cashflow, index|
|
18
|
+
total = total + cashflow / (1+rate) ** index
|
19
|
+
end
|
20
|
+
total
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
def sum
|
24
|
+
self.inject(:+)
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
def xirr
|
28
|
+
end
|
29
29
|
end
|
30
30
|
|
31
31
|
class Array
|
32
|
-
|
32
|
+
include Cashflow
|
33
33
|
end
|
data/lib/finance.rb
CHANGED
@@ -12,52 +12,52 @@ require 'timespan'
|
|
12
12
|
# * *rate* represents the interest rate _per period_.
|
13
13
|
module Finance
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
15
|
+
def Finance.payments(principal, rates)
|
16
|
+
p_total = rates.inject(0) { |sum, n| sum + n[0] }
|
17
|
+
p_current = 0
|
18
|
+
payments = []
|
19
|
+
|
20
|
+
rates.each do |periods, rate|
|
21
|
+
payment = Finance.pmt(principal, rate, p_total-p_current)
|
22
|
+
|
23
|
+
begin
|
24
|
+
payment = payment.round(2)
|
25
|
+
rescue ArgumentError
|
26
|
+
payment = (payment * 100.0).round / 100.0
|
27
|
+
end
|
28
|
+
|
29
|
+
if block_given?
|
30
|
+
payment = yield(payment)
|
31
|
+
end
|
32
|
+
|
33
|
+
periods.times do
|
34
|
+
interest = principal * rate
|
35
|
+
|
36
|
+
if payment > principal + interest
|
37
|
+
payment = principal + interest
|
38
|
+
end
|
39
|
+
|
40
|
+
principal = principal + interest - payment
|
41
|
+
|
42
|
+
begin
|
43
|
+
principal = principal.round(2)
|
44
|
+
rescue ArgumentError
|
45
|
+
principal = (principal * 100.0).round / 100.0
|
46
|
+
end
|
47
|
+
|
48
|
+
payments << payment
|
49
|
+
break if principal == 0
|
50
|
+
|
51
|
+
p_current = p_current + 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
payments
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return the number of periods needed to pay off a loan with the
|
59
|
+
# given payment.
|
60
|
+
def Finance.nper(payment, rate, principal)
|
61
|
+
-(Math.log(1-((principal/payment)*rate))) / Math.log(1+rate)
|
62
|
+
end
|
63
63
|
end
|
data/lib/rates.rb
CHANGED
@@ -2,88 +2,92 @@ require 'rubygems'
|
|
2
2
|
require 'flt'
|
3
3
|
|
4
4
|
class Rate
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
attr_accessor :duration
|
6
|
+
attr_accessor :periods
|
7
|
+
attr_accessor :nominal
|
8
|
+
|
9
|
+
def ==(rate)
|
10
|
+
self.nominal == rate.nominal and self.periods == rate.periods
|
11
|
+
end
|
8
12
|
|
9
13
|
# Alias method for *effective*.
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
def apr=(apr)
|
15
|
+
self.effective = apr
|
16
|
+
end
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
18
|
+
def apr
|
19
|
+
self.effective
|
20
|
+
end
|
17
21
|
|
18
22
|
# Alias method for *effective*.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
23
|
+
def apy=(apy)
|
24
|
+
self.effective = apy
|
25
|
+
end
|
26
|
+
|
27
|
+
def apy
|
28
|
+
self.effective
|
29
|
+
end
|
30
|
+
|
31
|
+
def effective=(rate)
|
32
|
+
unless @periods == Flt::DecNum.infinity
|
33
|
+
@nominal = @periods * ((1 + rate) ** (1 / @periods) - 1)
|
34
|
+
else
|
35
|
+
@nominal = Math.log(rate + 1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def effective
|
40
|
+
unless @periods == Flt::DecNum.infinity
|
41
|
+
(1 + @nominal / @periods) ** @periods - 1
|
42
|
+
else
|
43
|
+
@nominal.exp - 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(rate, type, opts={})
|
48
|
+
# Make sure the rate is a decimal.
|
49
|
+
unless rate.class == Flt::DecNum
|
50
|
+
rate = Flt::DecNum rate.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the compounding interval.
|
54
|
+
compounding = opts.fetch(:compounds, :monthly)
|
55
|
+
|
56
|
+
translate = {
|
57
|
+
:annually => Flt::DecNum(1),
|
58
|
+
:continuously => Flt::DecNum.infinity,
|
59
|
+
:daily => Flt::DecNum(365),
|
60
|
+
:monthly => Flt::DecNum(12),
|
61
|
+
:quarterly => Flt::DecNum(4),
|
62
|
+
:semiannually => Flt::DecNum(2)
|
63
|
+
}
|
64
|
+
|
65
|
+
if translate.has_key? compounding
|
66
|
+
@periods = translate.fetch compounding
|
67
|
+
elsif compounding.kind_of? Numeric
|
68
|
+
@periods = Flt::DecNum compounding.to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
# Set the rate in the proper way, based on the value of :type:.
|
72
|
+
if %w{apr apy effective}.include? type.to_s
|
73
|
+
self.effective = rate
|
74
|
+
else
|
75
|
+
@nominal = rate
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set the remainder of the attributes provided in :opts:.
|
79
|
+
opts.each do |key, value|
|
80
|
+
unless key == :compounds
|
81
|
+
send("#{key}=", value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
"Rate.new(#{self.apr.round(6)}, :apr)"
|
88
|
+
end
|
89
|
+
|
90
|
+
def monthly
|
91
|
+
(self.effective / 12).round(15)
|
92
|
+
end
|
89
93
|
end
|
data/lib/timespan.rb
CHANGED
data/test/test_amortization.rb
CHANGED
@@ -4,35 +4,168 @@ require 'flt/d'
|
|
4
4
|
require 'test/unit'
|
5
5
|
|
6
6
|
class TestBasicAmortization < Test::Unit::TestCase
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
7
|
+
def interest_in_period(principal, rate, payment, period)
|
8
|
+
-(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
13
|
+
@principal = D(200000)
|
14
|
+
@amortization = Amortization.new(@principal, @rate)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_balance
|
18
|
+
assert @amortization.balance.zero?
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_duration
|
22
|
+
assert_equal 360, @amortization.duration
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_interest
|
26
|
+
0.upto 359 do |period|
|
27
|
+
assert_equal @amortization.interest[period], interest_in_period(@principal, @rate.monthly, @amortization.payment, period+1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_interest_sum
|
32
|
+
assert_equal D('133443.53'), @amortization.interest.sum
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_payment
|
36
|
+
assert_equal D('-926.23'), @amortization.payment
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_payments
|
40
|
+
payments = [ D('-926.23') ] * @rate.duration
|
41
|
+
# Account for rounding errors in last payment.
|
42
|
+
payments[-1] = D('-926.96')
|
43
|
+
assert_equal payments, @amortization.payments
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_payments_sum
|
47
|
+
assert_equal D('-333443.53'), @amortization.payments.sum
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_principal
|
51
|
+
assert_equal @principal, @amortization.principal
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_sum
|
55
|
+
assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class TestAdjustableAmortization < Test::Unit::TestCase
|
60
|
+
def setup
|
61
|
+
@rates = []
|
62
|
+
0.upto 9 do |adj|
|
63
|
+
@rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3.years)
|
64
|
+
end
|
65
|
+
@principal = D(200000)
|
66
|
+
@amortization = Amortization.new(@principal, *@rates)
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_balance
|
70
|
+
assert @amortization.balance.zero?
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_duration
|
74
|
+
assert_equal 360, @amortization.duration
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_interest_sum
|
78
|
+
assert_equal D('277505.92'), @amortization.interest.sum
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_payment
|
82
|
+
assert_nil @amortization.payment
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_payments
|
86
|
+
values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 1601.78}
|
87
|
+
values.collect!{ |v| -D(v) }
|
88
|
+
|
89
|
+
payments = []
|
90
|
+
values[0,9].each do |v|
|
91
|
+
36.times do
|
92
|
+
payments << v
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
35.times { payments << values[9] }
|
97
|
+
payments << values[10]
|
98
|
+
|
99
|
+
payments.each_with_index do |payment, index|
|
100
|
+
assert_equal payment, @amortization.payments[index]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_payment_sum
|
105
|
+
assert_equal D('-477505.92'), @amortization.payments.sum
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_principal
|
109
|
+
assert_equal @principal, @amortization.principal
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_sum
|
113
|
+
assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class TestExtraPaymentAmortization < Test::Unit::TestCase
|
118
|
+
def setup
|
119
|
+
@rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
120
|
+
@principal = D(200000)
|
121
|
+
@amortization = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_additional_payments_sum
|
125
|
+
assert_equal D('-30084.86'), @amortization.additional_payments.sum
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_balance
|
129
|
+
assert @amortization.balance.zero?
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_duration
|
133
|
+
assert_equal 301, @amortization.duration
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_interest_sum
|
137
|
+
assert_equal D('108880.09'), @amortization.interest.sum
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_payment
|
141
|
+
assert_equal D('-1026.23'), @amortization.payment
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_payment_sum
|
145
|
+
assert_equal D('-308880.09'), @amortization.payments.sum
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_principal
|
149
|
+
assert_equal @principal, @amortization.principal
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_sum
|
153
|
+
assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class TestNumericMethod < Test::Unit::TestCase
|
158
|
+
def test_simple
|
159
|
+
rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
160
|
+
amt_method = 300000.amortize(rate)
|
161
|
+
amt_class = Amortization.new(300000, rate)
|
162
|
+
assert_equal amt_method, amt_class
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_with_block
|
166
|
+
rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
167
|
+
amt_method = 300000.amortize(rate){ |period| period.payment-300 }
|
168
|
+
amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
|
169
|
+
assert_equal amt_method, amt_class
|
170
|
+
end
|
38
171
|
end
|
data/test/test_cashflows.rb
CHANGED
@@ -2,13 +2,13 @@ require 'cashflows'
|
|
2
2
|
require 'test/unit'
|
3
3
|
|
4
4
|
class TestIRR < Test::Unit::TestCase
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
def test_simple
|
6
|
+
assert_in_delta(0.143, [-4000,1200,1410,1875,1050].irr, 0.001)
|
7
|
+
end
|
8
8
|
end
|
9
9
|
|
10
10
|
class TestNPV < Test::Unit::TestCase
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def test_simple
|
12
|
+
assert_in_delta(49.211, [-100.0, 60, 60, 60].npv(0.1), 0.001)
|
13
|
+
end
|
14
14
|
end
|
data/test/test_rates.rb
CHANGED
@@ -4,28 +4,28 @@ require 'flt/d'
|
|
4
4
|
require 'test/unit'
|
5
5
|
|
6
6
|
class TestRates < Test::Unit::TestCase
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def test_apr
|
8
|
+
rate = Rate.new(0.0375, :apr)
|
9
|
+
assert_equal D('0.03687'), rate.nominal.round(5)
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
12
|
+
def test_duration
|
13
|
+
rate = Rate.new(0.0375, :effective, :duration => 30.years)
|
14
|
+
assert_equal 360, rate.duration
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_effective
|
18
|
+
rate = Rate.new(0.03687, :nominal)
|
19
|
+
assert_equal D('0.0375'), rate.effective.round(4)
|
20
|
+
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def test_monthly
|
23
|
+
rate = Rate.new(0.0375, :effective)
|
24
|
+
assert_equal D('0.003125'), rate.monthly
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def test_nominal
|
28
|
+
rate = Rate.new(0.0375, :effective)
|
29
|
+
assert_equal D('0.03687'), rate.nominal.round(5)
|
30
|
+
end
|
31
31
|
end
|
data/test/test_timespan.rb
CHANGED
@@ -2,11 +2,11 @@ require 'finance'
|
|
2
2
|
require 'test/unit'
|
3
3
|
|
4
4
|
class TestTime < Test::Unit::TestCase
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
def test_months
|
6
|
+
assert_equal 360, 360.months
|
7
|
+
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
def test_years
|
10
|
+
assert_equal 360, 30.years
|
11
|
+
end
|
12
12
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finance
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Bill Kranec
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-06-
|
18
|
+
date: 2011-06-28 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: flt
|