finance 0.1.1 → 0.2.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.
- 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
|