finance 1.1.2 → 2.0.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/HISTORY +8 -0
- data/{README → README.md} +63 -52
- data/lib/finance.rb +1 -1
- data/lib/finance/amortization.rb +16 -16
- data/lib/finance/cashflows.rb +7 -7
- data/lib/finance/rates.rb +10 -10
- data/lib/finance/transaction.rb +3 -3
- data/test/test_amortization.rb +41 -48
- data/test/test_cashflows.rb +14 -20
- data/test/test_helper.rb +13 -0
- data/test/test_rates.rb +18 -24
- metadata +49 -13
- data/lib/finance/interval.rb +0 -13
- data/test/test_interval.rb +0 -18
data/HISTORY
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
= Version 2.0.0
|
2
|
+
23 Jul 2013
|
3
|
+
|
4
|
+
* Removed Integer#months, Integer#years, and replaced Numeric#to_d by Numeric#to_s in the interest of Rails compatibility.
|
5
|
+
* Converted unit tests from the shoulda framework to minitest.
|
6
|
+
* Removed octal numbers in test_cashflow.rb
|
7
|
+
* Thanks to @thadd, @bramswenson, and @xpe for their contributions to this release!
|
8
|
+
|
1
9
|
= Version 1.1.2
|
2
10
|
16 Jun 2012
|
3
11
|
|
data/{README → README.md}
RENAMED
@@ -1,104 +1,115 @@
|
|
1
|
-
|
1
|
+
# FINANCE
|
2
2
|
|
3
3
|
a library for financial modelling in Ruby.
|
4
4
|
|
5
|
-
|
5
|
+
## INSTALL
|
6
6
|
|
7
|
-
|
7
|
+
$ sudo gem install finance
|
8
8
|
|
9
|
-
|
9
|
+
## IMPORTANT CHANGES
|
10
10
|
|
11
|
-
|
11
|
+
Contributions by [@thadd](https://github.com/thadd) and
|
12
|
+
[@bramswenson](https://github.com/bramswenson) have made the `finance`
|
13
|
+
library fully compatible with rails, at the cost of the `#years` and
|
14
|
+
`#months` convenience methods on `Integer`, as well as the `#to_d` method for
|
15
|
+
converting `Numerics` into `DecNums`. These methods have been removed, due to
|
16
|
+
conflicts with existing rails methods.
|
12
17
|
|
13
|
-
|
18
|
+
Correspondingly, `finance` has been bumped up to version 2.0.
|
19
|
+
|
20
|
+
## OVERVIEW
|
21
|
+
|
22
|
+
### GETTING STARTED
|
23
|
+
|
24
|
+
>> require 'finance'
|
14
25
|
|
15
26
|
*Note:* As of version 1.0.0, the entire library is contained under the
|
16
27
|
Finance namespace. Existing code will not work unless you add:
|
17
28
|
|
18
|
-
|
29
|
+
>> include Finance
|
19
30
|
|
20
31
|
for all of the examples below, we'll assume that you have done this.
|
21
32
|
|
22
|
-
|
33
|
+
### AMORTIZATION
|
23
34
|
|
24
35
|
You are interested in borrowing $250,000 under a 30 year, fixed-rate
|
25
36
|
loan with a 4.25% APR.
|
26
37
|
|
27
|
-
|
28
|
-
|
38
|
+
>> rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
|
39
|
+
>> amortization = Amortization.new(250000, rate)
|
29
40
|
|
30
41
|
Find the standard monthly payment:
|
31
42
|
|
32
|
-
|
33
|
-
|
43
|
+
>> amortization.payment
|
44
|
+
=> DecNum('-1229.91')
|
34
45
|
|
35
46
|
Find the total cost of the loan:
|
36
47
|
|
37
|
-
|
38
|
-
|
48
|
+
>> amortization.payments.sum
|
49
|
+
=> DecNum('-442766.55')
|
39
50
|
|
40
51
|
How much will you pay in interest?
|
41
52
|
|
42
|
-
|
43
|
-
|
53
|
+
>> amortization.interest.sum
|
54
|
+
=> DecNum('192766.55')
|
44
55
|
|
45
56
|
How much interest in the first six months?
|
46
57
|
|
47
|
-
|
48
|
-
|
58
|
+
>> amortization.interest[0,6].sum
|
59
|
+
=> DecNum('5294.62')
|
49
60
|
|
50
61
|
If your loan has an adjustable rate, no problem. You can pass an
|
51
62
|
arbitrary number of rates, and they will be used in the amortization.
|
52
63
|
For example, we can look at an amortization of $250000, where the APR
|
53
64
|
starts at 4.25%, and increases by 1% every five years.
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
|
66
|
+
>> values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
|
67
|
+
>> rates = values.collect { |value| Rate.new( value, :apr, :duration => (5 * 12) }
|
68
|
+
>> arm = Amortization.new(250000, *rates)
|
58
69
|
|
59
70
|
Since we are looking at an ARM, there is no longer a single "payment" value.
|
60
71
|
|
61
|
-
|
62
|
-
|
72
|
+
>> arm.payment
|
73
|
+
=> nil
|
63
74
|
|
64
75
|
But we can look at the different payments over time.
|
65
76
|
|
66
|
-
|
67
|
-
|
77
|
+
>> arm.payments.uniq
|
78
|
+
=> [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), ... snipped ... ]
|
68
79
|
|
69
80
|
The other methods previously discussed can be accessed in the same way:
|
70
81
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
82
|
+
>> arm.interest.sum
|
83
|
+
=> DecNum('287515.45')
|
84
|
+
>> arm.payments.sum
|
85
|
+
=> DecNum('-537515.45')
|
75
86
|
|
76
87
|
Last, but not least, you may pass a block when creating an Amortization
|
77
88
|
which returns a modified monthly payment. For example, to increase your
|
78
89
|
payment by $150, do:
|
79
90
|
|
80
|
-
|
81
|
-
|
91
|
+
>> rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
|
92
|
+
>> extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
|
82
93
|
|
83
94
|
Disregarding the block, we have used the same parameters as the first
|
84
95
|
example. Notice the difference in the results:
|
85
96
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
>> amortization.payments.sum
|
98
|
+
=> DecNum('-442745.98')
|
99
|
+
>> extra_payments.payments.sum
|
100
|
+
=> DecNum('-400566.24')
|
101
|
+
>> amortization.interest.sum
|
102
|
+
=> DecNum('192745.98')
|
103
|
+
>> extra_payments.interest.sum
|
104
|
+
=> DecNum('150566.24')
|
94
105
|
|
95
106
|
You can also increase your payment to a specific amount:
|
96
107
|
|
97
|
-
|
108
|
+
>> extra_payments_2 = 250000.amortize(rate){ -1500 }
|
98
109
|
|
99
|
-
|
110
|
+
## ABOUT
|
100
111
|
|
101
|
-
I started developing
|
112
|
+
I started developing `finance` while analyzing mortgages as a personal
|
102
113
|
project. Spreadsheets have convenient formulas for doing this type of
|
103
114
|
work, until you want to do something semi-complex (like ARMs or extra
|
104
115
|
payments), at which point you need to create your own amortization
|
@@ -110,31 +121,31 @@ have as a gem.
|
|
110
121
|
More broadly, I believe there are many calculations that are necessary
|
111
122
|
for the effective management of personal finances, but are difficult
|
112
123
|
(or impossible) to do with spreadsheets or other existing open source
|
113
|
-
tools. My hope is that the
|
124
|
+
tools. My hope is that the `finance` library will grow to provide a set
|
114
125
|
of open, tested tools to fill this gap.
|
115
126
|
|
116
|
-
If you have used
|
127
|
+
If you have used `finance` and find it useful, I would enjoy hearing
|
117
128
|
about it!
|
118
129
|
|
119
|
-
|
130
|
+
## FEATURES
|
120
131
|
|
121
132
|
Currently implemented features include:
|
122
133
|
|
123
|
-
* Uses the
|
134
|
+
* Uses the [flt](http://flt.rubyforge.org/) library to ensure precision decimal arithmetic in all calculations.
|
124
135
|
* Fixed-rate mortgage amortization (30/360).
|
125
136
|
* Interest rates
|
126
137
|
* Various cash flow computations, such as NPV and IRR.
|
127
138
|
* Adjustable rate mortgage amortization.
|
128
139
|
* Payment modifications (i.e., how does paying an additional $75 per month affect the amortization?)
|
129
140
|
|
130
|
-
|
141
|
+
## RESOURCES
|
131
142
|
|
132
|
-
[RubyGems Page]
|
133
|
-
[Source Code]
|
134
|
-
[Bug Tracker]
|
135
|
-
[Google Group]
|
143
|
+
* [RubyGems Page](https://rubygems.org/gems/finance)
|
144
|
+
* [Source Code](http://github.com/wkranec/finance)
|
145
|
+
* [Bug Tracker](https://github.com/wkranec/finance/issues)
|
146
|
+
* [Google Group](http://groups.google.com/group/finance-gem/topics?pli=1)
|
136
147
|
|
137
|
-
|
148
|
+
## COPYRIGHT
|
138
149
|
|
139
150
|
This library is released under the terms of the LGPL license.
|
140
151
|
|
data/lib/finance.rb
CHANGED
data/lib/finance/amortization.rb
CHANGED
@@ -8,14 +8,14 @@ module Finance
|
|
8
8
|
# example uses the amortize method for the Numeric class. The second
|
9
9
|
# calls Amortization.new directly.
|
10
10
|
# @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR
|
11
|
-
# rate = Rate.new(0.0425, :apr, :duration => 30
|
11
|
+
# rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
|
12
12
|
# amortization = 250000.amortize(rate)
|
13
13
|
# @example Borrow $250,000 under a 30 year, adjustable rate loan, with an APR starting at 4.25%, and increasing by 1% every five years
|
14
14
|
# values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
|
15
|
-
# rates = values.collect { |value| Rate.new( value, :apr, :duration = 5
|
15
|
+
# rates = values.collect { |value| Rate.new( value, :apr, :duration = (5 * 12) ) }
|
16
16
|
# arm = Amortization.new(250000, *rates)
|
17
17
|
# @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR, but pay $150 extra each month
|
18
|
-
# rate = Rate.new(0.0425, :apr, :duration =>
|
18
|
+
# rate = Rate.new(0.0425, :apr, :duration => (5 * 12))
|
19
19
|
# extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
|
20
20
|
# @api public
|
21
21
|
class Amortization
|
@@ -42,7 +42,7 @@ module Finance
|
|
42
42
|
|
43
43
|
# @return [Array] the amount of any additional payments in each period
|
44
44
|
# @example
|
45
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
45
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
46
46
|
# amt = 300000.amortize(rate){ |payment| payment.amount-100}
|
47
47
|
# amt.additional_payments #=> [DecNum('-100.00'), DecNum('-100.00'), ... ]
|
48
48
|
# @api public
|
@@ -63,8 +63,8 @@ module Finance
|
|
63
63
|
|
64
64
|
pmt = Payment.new(amount, :period => @period)
|
65
65
|
if @block then pmt.modify(&@block) end
|
66
|
-
|
67
|
-
rate.duration.times do
|
66
|
+
|
67
|
+
rate.duration.to_i.times do
|
68
68
|
# Do this first in case the balance is zero already.
|
69
69
|
if @balance.zero? then break end
|
70
70
|
|
@@ -78,7 +78,7 @@ module Finance
|
|
78
78
|
if pmt.amount.abs > @balance then pmt.amount = -@balance end
|
79
79
|
@transactions << pmt.dup
|
80
80
|
@balance += pmt.amount
|
81
|
-
|
81
|
+
|
82
82
|
@period += 1
|
83
83
|
end
|
84
84
|
end
|
@@ -111,11 +111,11 @@ module Finance
|
|
111
111
|
|
112
112
|
# @return [Integer] the time required to pay off the loan, in months
|
113
113
|
# @example In most cases, the duration is equal to the total duration of all rates
|
114
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
114
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
115
115
|
# amt = 300000.amortize(rate)
|
116
116
|
# amt.duration #=> 360
|
117
117
|
# @example Extra payments may reduce the duration
|
118
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
118
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
119
119
|
# amt = 300000.amortize(rate){ |payment| payment.amount-100}
|
120
120
|
# amt.duration #=> 319
|
121
121
|
# @api public
|
@@ -130,10 +130,10 @@ module Finance
|
|
130
130
|
# @param [Proc] block
|
131
131
|
# @api public
|
132
132
|
def initialize(principal, *rates, &block)
|
133
|
-
@principal = principal.
|
133
|
+
@principal = Flt::DecNum.new(principal.to_s)
|
134
134
|
@rates = rates
|
135
135
|
@block = block
|
136
|
-
|
136
|
+
|
137
137
|
# compute the total duration from all of the rates.
|
138
138
|
@periods = (rates.collect { |r| r.duration }).sum
|
139
139
|
@period = 0
|
@@ -148,11 +148,11 @@ module Finance
|
|
148
148
|
|
149
149
|
# @return [Array] the amount of interest charged in each period
|
150
150
|
# @example find the total cost of interest for a loan
|
151
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
151
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
152
152
|
# amt = 300000.amortize(rate)
|
153
153
|
# amt.interest.sum #=> DecNum('200163.94')
|
154
154
|
# @example find the total interest charges in the first six months
|
155
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
155
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
156
156
|
# amt = 300000.amortize(rate)
|
157
157
|
# amt.interest[0,6].sum #=> DecNum('5603.74')
|
158
158
|
# @api public
|
@@ -166,7 +166,7 @@ module Finance
|
|
166
166
|
# @param [Integer] periods the number of periods needed for repayment
|
167
167
|
# @note in most cases, you will probably want to use rate.monthly when calling this function outside of an Amortization instance.
|
168
168
|
# @example
|
169
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
169
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
170
170
|
# rate.duration #=> 360
|
171
171
|
# Amortization.payment(200000, rate.monthly, rate.duration) #=> DecNum('-926.23')
|
172
172
|
# @see http://en.wikipedia.org/wiki/Amortization_calculator
|
@@ -177,7 +177,7 @@ module Finance
|
|
177
177
|
|
178
178
|
# @return [Array] the amount of the payment in each period
|
179
179
|
# @example find the total payments for a loan
|
180
|
-
# rate = Rate.new(0.0375, :apr, :duration => 30
|
180
|
+
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
181
181
|
# amt = 300000.amortize(rate)
|
182
182
|
# amt.payments.sum #=> DecNum('-500163.94')
|
183
183
|
# @api public
|
@@ -191,6 +191,6 @@ class Numeric
|
|
191
191
|
# @see Amortization#new
|
192
192
|
# @api public
|
193
193
|
def amortize(*rates, &block)
|
194
|
-
|
194
|
+
Finance::Amortization.new(self, *rates, &block)
|
195
195
|
end
|
196
196
|
end
|
data/lib/finance/cashflows.rb
CHANGED
@@ -32,7 +32,7 @@ module Finance
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def values(x)
|
35
|
-
value = @transactions.send(@function, x[0].
|
35
|
+
value = @transactions.send(@function, Flt::DecNum.new(x[0].to_s))
|
36
36
|
[ BigDecimal.new(value.to_s) ]
|
37
37
|
end
|
38
38
|
end
|
@@ -52,7 +52,7 @@ module Finance
|
|
52
52
|
|
53
53
|
func = Function.new(self, :npv)
|
54
54
|
rate = [ func.one ]
|
55
|
-
|
55
|
+
nlsolve( func, rate )
|
56
56
|
rate[0]
|
57
57
|
end
|
58
58
|
|
@@ -69,9 +69,9 @@ module Finance
|
|
69
69
|
# @see http://en.wikipedia.org/wiki/Net_present_value
|
70
70
|
# @api public
|
71
71
|
def npv(rate)
|
72
|
-
self.collect! { |entry| entry.
|
72
|
+
self.collect! { |entry| Flt::DecNum.new(entry.to_s) }
|
73
73
|
|
74
|
-
rate, total = rate.
|
74
|
+
rate, total = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(0.to_s)
|
75
75
|
self.each_with_index do |cashflow, index|
|
76
76
|
total += cashflow / (1 + rate) ** index
|
77
77
|
end
|
@@ -97,7 +97,7 @@ module Finance
|
|
97
97
|
|
98
98
|
func = Function.new(self, :xnpv)
|
99
99
|
rate = [ func.one ]
|
100
|
-
|
100
|
+
nlsolve( func, rate )
|
101
101
|
Rate.new(rate[0], :apr, :compounds => :annually)
|
102
102
|
end
|
103
103
|
|
@@ -111,11 +111,11 @@ module Finance
|
|
111
111
|
# @transactions.xnpv(0.6).round(2) #=> -937.41
|
112
112
|
# @api public
|
113
113
|
def xnpv(rate)
|
114
|
-
rate = rate.
|
114
|
+
rate = Flt::DecNum.new(rate.to_s)
|
115
115
|
start = self[0].date
|
116
116
|
|
117
117
|
self.inject(0) do |sum, t|
|
118
|
-
n = t.amount / ( (1 + rate) ** ((t.date-start) / 31536000.
|
118
|
+
n = t.amount / ( (1 + rate) ** ((t.date-start) / Flt::DecNum.new(31536000.to_s))) # 365 * 86400
|
119
119
|
sum + n
|
120
120
|
end
|
121
121
|
end
|
data/lib/finance/rates.rb
CHANGED
@@ -11,7 +11,7 @@ module Finance
|
|
11
11
|
TYPES = { :apr => "effective",
|
12
12
|
:apy => "effective",
|
13
13
|
:effective => "effective",
|
14
|
-
:nominal => "nominal"
|
14
|
+
:nominal => "nominal"
|
15
15
|
}
|
16
16
|
|
17
17
|
# @return [Integer] the duration for which the rate is valid, in months
|
@@ -55,13 +55,13 @@ module Finance
|
|
55
55
|
# @api private
|
56
56
|
def compounds=(input)
|
57
57
|
@periods = case input
|
58
|
-
when :annually then Flt::DecNum
|
58
|
+
when :annually then Flt::DecNum.new(1)
|
59
59
|
when :continuously then Flt::DecNum.infinity
|
60
|
-
when :daily then Flt::DecNum
|
61
|
-
when :monthly then Flt::DecNum
|
62
|
-
when :quarterly then Flt::DecNum
|
63
|
-
when :semiannually then Flt::DecNum
|
64
|
-
when Numeric then Flt::DecNum
|
60
|
+
when :daily then Flt::DecNum.new(365)
|
61
|
+
when :monthly then Flt::DecNum.new(12)
|
62
|
+
when :quarterly then Flt::DecNum.new(4)
|
63
|
+
when :semiannually then Flt::DecNum.new(2)
|
64
|
+
when Numeric then Flt::DecNum.new(input.to_s)
|
65
65
|
else raise ArgumentError
|
66
66
|
end
|
67
67
|
end
|
@@ -98,7 +98,7 @@ module Finance
|
|
98
98
|
|
99
99
|
# Set the rate in the proper way, based on the value of type.
|
100
100
|
begin
|
101
|
-
send("#{TYPES.fetch(type)}=", rate.
|
101
|
+
send("#{TYPES.fetch(type)}=", Flt::DecNum.new(rate.to_s))
|
102
102
|
rescue KeyError
|
103
103
|
raise ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller
|
104
104
|
end
|
@@ -135,7 +135,7 @@ module Finance
|
|
135
135
|
# Rate.to_effective(0.05, 4) #=> DecNum('0.05095')
|
136
136
|
# @api public
|
137
137
|
def Rate.to_effective(rate, periods)
|
138
|
-
rate, periods = rate.
|
138
|
+
rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
|
139
139
|
|
140
140
|
if periods.infinite?
|
141
141
|
rate.exp - 1
|
@@ -153,7 +153,7 @@ module Finance
|
|
153
153
|
# @see http://www.miniwebtool.com/nominal-interest-rate-calculator/
|
154
154
|
# @api public
|
155
155
|
def Rate.to_nominal(rate, periods)
|
156
|
-
rate, periods = rate.
|
156
|
+
rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
|
157
157
|
|
158
158
|
if periods.infinite?
|
159
159
|
(rate + 1).log
|
data/lib/finance/transaction.rb
CHANGED
@@ -24,7 +24,7 @@ module Finance
|
|
24
24
|
# t.amount #=> 750
|
25
25
|
# @api public
|
26
26
|
def amount=(value)
|
27
|
-
@amount = value.
|
27
|
+
@amount = Flt::DecNum.new(value.to_s)
|
28
28
|
end
|
29
29
|
|
30
30
|
# @return [DecNum] the difference between the original transaction
|
@@ -51,7 +51,7 @@ module Finance
|
|
51
51
|
def initialize(amount, opts={})
|
52
52
|
@amount = amount
|
53
53
|
@original = amount
|
54
|
-
|
54
|
+
|
55
55
|
# Set optional attributes..
|
56
56
|
opts.each do |key, value|
|
57
57
|
send("#{key}=", value)
|
@@ -111,7 +111,7 @@ module Finance
|
|
111
111
|
"Interest(#{@amount})"
|
112
112
|
end
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
# Represent a loan payment as a Transaction
|
116
116
|
# @see Transaction
|
117
117
|
class Payment < Transaction
|
data/test/test_amortization.rb
CHANGED
@@ -1,91 +1,84 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '../lib/finance/interval.rb'
|
3
|
-
require_relative '../lib/finance/rates.rb'
|
4
|
-
include Finance
|
5
|
-
|
6
|
-
require 'flt/d'
|
7
|
-
require 'minitest/unit'
|
8
|
-
require 'shoulda'
|
1
|
+
require_relative 'test_helper'
|
9
2
|
|
10
3
|
# @see http://tinyurl.com/6zroqvd for detailed calculations for the
|
11
4
|
# examples in these unit tests.
|
12
|
-
|
5
|
+
describe "Amortization" do
|
13
6
|
def ipmt(principal, rate, payment, period)
|
14
7
|
-(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
|
15
8
|
end
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
@rate = Rate.new(0.0375, :apr, :duration => 30
|
10
|
+
describe "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
|
11
|
+
before(:all) do
|
12
|
+
@rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
20
13
|
@principal = D(200000)
|
21
14
|
@std = Amortization.new(@principal, @rate)
|
22
15
|
end
|
23
16
|
|
24
|
-
should
|
17
|
+
it "should have a principal of $200,000" do
|
25
18
|
assert_equal @principal, @std.principal
|
26
19
|
end
|
27
20
|
|
28
|
-
should
|
21
|
+
it "should have a final balance of zero" do
|
29
22
|
assert @std.balance.zero?
|
30
23
|
end
|
31
24
|
|
32
|
-
should
|
25
|
+
it "should have a duration of 360 months" do
|
33
26
|
assert_equal 360, @std.duration
|
34
27
|
end
|
35
28
|
|
36
|
-
should
|
29
|
+
it "should have a monthly payment of $926.23" do
|
37
30
|
assert_equal D('-926.23'), @std.payment
|
38
31
|
end
|
39
32
|
|
40
|
-
should
|
33
|
+
it "should have a final payment of $926.96 (due to rounding)" do
|
41
34
|
assert_equal D('-926.96'), @std.payments[-1]
|
42
35
|
end
|
43
36
|
|
44
|
-
should
|
37
|
+
it "should have total payments of $333,443.53" do
|
45
38
|
assert_equal D('-333443.53'), @std.payments.sum
|
46
39
|
end
|
47
40
|
|
48
|
-
should
|
41
|
+
it "should have interest charges which agree with the standard formula" do
|
49
42
|
0.upto 359 do |period|
|
50
43
|
assert_equal @std.interest[period], ipmt(@principal, @rate.monthly, @std.payment, period+1)
|
51
44
|
end
|
52
45
|
end
|
53
46
|
|
54
|
-
should
|
47
|
+
it "should have total interest charges of $133,433.33" do
|
55
48
|
assert_equal D('133443.53'), @std.interest.sum
|
56
49
|
end
|
57
50
|
end
|
58
51
|
|
59
|
-
|
60
|
-
|
52
|
+
describe "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
|
53
|
+
before(:all) do
|
61
54
|
@rates = []
|
62
55
|
0.upto 9 do |adj|
|
63
|
-
@rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3
|
56
|
+
@rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => (3 * 12))
|
64
57
|
end
|
65
58
|
@principal = D(200000)
|
66
59
|
@arm = Amortization.new(@principal, *@rates)
|
67
60
|
end
|
68
61
|
|
69
|
-
should
|
62
|
+
it "should have a principal of $200,000" do
|
70
63
|
assert_equal @principal, @arm.principal
|
71
64
|
end
|
72
65
|
|
73
|
-
should
|
66
|
+
it "should have a final balance of zero" do
|
74
67
|
assert @arm.balance.zero?
|
75
68
|
end
|
76
69
|
|
77
|
-
should
|
70
|
+
it "should have a duration of 360 months" do
|
78
71
|
assert_equal 360, @arm.duration
|
79
72
|
end
|
80
73
|
|
81
|
-
should
|
74
|
+
it "should not have a fixed monthly payment (since it changes)" do
|
82
75
|
assert_nil @arm.payment
|
83
76
|
end
|
84
77
|
|
85
|
-
should
|
78
|
+
it "should have payments which increase every three years" do
|
86
79
|
values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 }
|
87
80
|
values.collect!{ |v| -D(v) }
|
88
|
-
|
81
|
+
|
89
82
|
payments = []
|
90
83
|
values[0,9].each do |v|
|
91
84
|
36.times do
|
@@ -100,70 +93,70 @@ class TestAmortization < Test::Unit::TestCase
|
|
100
93
|
end
|
101
94
|
end
|
102
95
|
|
103
|
-
should
|
96
|
+
it "should have a final payment of $1601.78 (due to rounding)" do
|
104
97
|
assert_equal D('-1601.78'), @arm.payments[-1]
|
105
98
|
end
|
106
99
|
|
107
|
-
should
|
100
|
+
it "should have total payments of $47,505.92" do
|
108
101
|
assert_equal D('-477505.92'), @arm.payments.sum
|
109
102
|
end
|
110
103
|
|
111
|
-
should
|
104
|
+
it "should have total interest charges of $277,505.92" do
|
112
105
|
assert_equal D('277505.92'), @arm.interest.sum
|
113
106
|
end
|
114
107
|
end
|
115
108
|
|
116
|
-
|
117
|
-
|
118
|
-
@rate = Rate.new(0.0375, :apr, :duration => 30
|
109
|
+
describe "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
|
110
|
+
before(:all) do
|
111
|
+
@rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
119
112
|
@principal = D(200000)
|
120
113
|
@exp = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
|
121
114
|
end
|
122
115
|
|
123
|
-
should
|
116
|
+
it "should have a principal of $200,000" do
|
124
117
|
assert_equal @principal, @exp.principal
|
125
118
|
end
|
126
119
|
|
127
|
-
should
|
120
|
+
it "should have a final balance of zero" do
|
128
121
|
assert @exp.balance.zero?
|
129
122
|
end
|
130
123
|
|
131
|
-
should
|
124
|
+
it "should have a duration of 301 months" do
|
132
125
|
assert_equal 301, @exp.duration
|
133
126
|
end
|
134
127
|
|
135
|
-
should
|
128
|
+
it "should have a monthly payment of $1026.23" do
|
136
129
|
assert_equal D('-1026.23'), @exp.payment
|
137
130
|
end
|
138
131
|
|
139
|
-
should
|
132
|
+
it "should have a final payment of $1011.09" do
|
140
133
|
assert_equal D('-1011.09'), @exp.payments[-1]
|
141
134
|
end
|
142
135
|
|
143
|
-
should
|
136
|
+
it "should have total payments of $308,880.09" do
|
144
137
|
assert_equal D('-308880.09'), @exp.payments.sum
|
145
138
|
end
|
146
139
|
|
147
|
-
should
|
140
|
+
it "should have total additional payments of $30,084.86" do
|
148
141
|
assert_equal D('-30084.86'), @exp.additional_payments.sum
|
149
142
|
end
|
150
143
|
|
151
|
-
should
|
144
|
+
it "should have total interest charges of $108880.09" do
|
152
145
|
assert_equal D('108880.09'), @exp.interest.sum
|
153
146
|
end
|
154
147
|
end
|
155
148
|
end
|
156
149
|
|
157
|
-
|
158
|
-
|
159
|
-
rate = Rate.new(0.0375, :apr, :duration => 30
|
150
|
+
describe "Numeric Method" do
|
151
|
+
it 'works with simple invocation' do
|
152
|
+
rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
160
153
|
amt_method = 300000.amortize(rate)
|
161
154
|
amt_class = Amortization.new(300000, rate)
|
162
155
|
assert_equal amt_method, amt_class
|
163
156
|
end
|
164
157
|
|
165
|
-
|
166
|
-
rate = Rate.new(0.0375, :apr, :duration => 30
|
158
|
+
it 'works with block invocation' do
|
159
|
+
rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
167
160
|
amt_method = 300000.amortize(rate){ |period| period.payment-300 }
|
168
161
|
amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
|
169
162
|
assert_equal amt_method, amt_class
|
data/test/test_cashflows.rb
CHANGED
@@ -1,37 +1,31 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '../lib/finance/rates.rb'
|
3
|
-
require_relative '../lib/finance/transaction.rb'
|
4
|
-
include Finance
|
1
|
+
require_relative 'test_helper'
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
class TestCashflows < Test::Unit::TestCase
|
11
|
-
context "an array of numeric cashflows" do
|
12
|
-
should "have an Internal Rate of Return" do
|
3
|
+
describe "Cashflows" do
|
4
|
+
describe "an array of numeric cashflows" do
|
5
|
+
it "should have an Internal Rate of Return" do
|
13
6
|
assert_equal D("0.143"), [-4000,1200,1410,1875,1050].irr.round(3)
|
14
7
|
assert_raises(ArgumentError) { [10,20,30].irr }
|
15
8
|
end
|
16
9
|
|
17
|
-
should
|
10
|
+
it "should have a Net Present Value" do
|
18
11
|
assert_equal D("49.211"), [-100.0, 60, 60, 60].npv(0.1).round(3)
|
19
12
|
end
|
20
13
|
end
|
21
|
-
|
22
|
-
|
14
|
+
|
15
|
+
describe "an array of Transactions" do
|
16
|
+
before(:all) do
|
23
17
|
@xactions=[]
|
24
|
-
@xactions << Transaction.new(-1000, :date => Time.new(1985,
|
25
|
-
@xactions << Transaction.new( 600, :date => Time.new(1990,
|
26
|
-
@xactions << Transaction.new( 600, :date => Time.new(1995,
|
18
|
+
@xactions << Transaction.new(-1000, :date => Time.new(1985, 1, 1))
|
19
|
+
@xactions << Transaction.new( 600, :date => Time.new(1990, 1, 1))
|
20
|
+
@xactions << Transaction.new( 600, :date => Time.new(1995, 1, 1))
|
27
21
|
end
|
28
22
|
|
29
|
-
should
|
23
|
+
it "should have an Internal Rate of Return" do
|
30
24
|
assert_equal D("0.024851"), @xactions.xirr.effective.round(6)
|
31
|
-
assert_raises(ArgumentError) { @xactions[1,2].xirr }
|
25
|
+
assert_raises(ArgumentError) { @xactions[1, 2].xirr }
|
32
26
|
end
|
33
27
|
|
34
|
-
should
|
28
|
+
it "should have a Net Present Value" do
|
35
29
|
assert_equal D("-937.41"), @xactions.xnpv(0.6).round(2)
|
36
30
|
end
|
37
31
|
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/spec'
|
3
|
+
|
4
|
+
require 'active_support/all'
|
5
|
+
|
6
|
+
require 'flt'
|
7
|
+
require 'flt/d'
|
8
|
+
|
9
|
+
require_relative '../lib/finance/amortization.rb'
|
10
|
+
require_relative '../lib/finance/cashflows.rb'
|
11
|
+
require_relative '../lib/finance/rates.rb'
|
12
|
+
require_relative '../lib/finance/transaction.rb'
|
13
|
+
include Finance
|
data/test/test_rates.rb
CHANGED
@@ -1,77 +1,71 @@
|
|
1
|
-
require_relative '
|
2
|
-
include Finance
|
1
|
+
require_relative 'test_helper'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
class TestRates < Test::Unit::TestCase
|
10
|
-
context "an interest rate" do
|
11
|
-
context "can compound with different periods" do
|
12
|
-
should "compound monthly by default" do
|
3
|
+
describe "Rates" do
|
4
|
+
describe "an interest rate" do
|
5
|
+
describe "can compound with different periods" do
|
6
|
+
it "should compound monthly by default" do
|
13
7
|
rate = Rate.new(0.15, :nominal)
|
14
8
|
assert_equal D('0.16075'), rate.effective.round(5)
|
15
9
|
end
|
16
10
|
|
17
|
-
should
|
11
|
+
it "should compound annually" do
|
18
12
|
rate = Rate.new(0.15, :nominal, :compounds => :annually)
|
19
13
|
assert_equal D('0.15'), rate.effective
|
20
14
|
end
|
21
15
|
|
22
|
-
should
|
16
|
+
it "should compound continuously" do
|
23
17
|
rate = Rate.new(0.15, :nominal, :compounds => :continuously)
|
24
18
|
assert_equal D('0.16183'), rate.effective.round(5)
|
25
19
|
end
|
26
20
|
|
27
|
-
should
|
21
|
+
it "should compound daily" do
|
28
22
|
rate = Rate.new(0.15, :nominal, :compounds => :daily)
|
29
23
|
assert_equal D('0.16180'), rate.effective.round(5)
|
30
24
|
end
|
31
25
|
|
32
|
-
should
|
26
|
+
it "should compound quarterly" do
|
33
27
|
rate = Rate.new(0.15, :nominal, :compounds => :quarterly)
|
34
28
|
assert_equal D('0.15865'), rate.effective.round(5)
|
35
29
|
end
|
36
30
|
|
37
|
-
should
|
31
|
+
it "should compound semiannually" do
|
38
32
|
rate = Rate.new(0.15, :nominal, :compounds => :semiannually)
|
39
33
|
assert_equal D('0.15563'), rate.effective.round(5)
|
40
34
|
end
|
41
35
|
|
42
|
-
should
|
36
|
+
it "should accept a numerical value as the compounding frequency per year" do
|
43
37
|
rate = Rate.new(0.15, :nominal, :compounds => 7)
|
44
38
|
assert_equal D('0.15999'), rate.effective.round(5)
|
45
39
|
end
|
46
40
|
|
47
|
-
should
|
41
|
+
it "should raise an exception if an unknown string is given" do
|
48
42
|
assert_raises(ArgumentError){ Rate.new(0.15, :nominal, :compounds => :quickly) }
|
49
43
|
end
|
50
44
|
end
|
51
45
|
|
52
|
-
should
|
46
|
+
it "should accept a duration if given" do
|
53
47
|
rate = Rate.new(0.0375, :effective, :duration => 360)
|
54
48
|
assert_equal 360, rate.duration
|
55
49
|
end
|
56
50
|
|
57
|
-
should
|
51
|
+
it "should be comparable to other interest rates" do
|
58
52
|
r1 = Rate.new(0.15, :nominal)
|
59
53
|
r2 = Rate.new(0.16, :nominal)
|
60
54
|
assert_equal( 1, r2 <=> r1)
|
61
55
|
assert_equal(-1, r1 <=> r2)
|
62
56
|
end
|
63
|
-
|
64
|
-
should
|
57
|
+
|
58
|
+
it "should convert to a monthly value" do
|
65
59
|
rate = Rate.new(0.0375, :effective)
|
66
60
|
assert_equal D('0.003125'), rate.monthly
|
67
61
|
end
|
68
62
|
|
69
|
-
should
|
63
|
+
it "should convert effective interest rates to nominal" do
|
70
64
|
assert_equal D('0.03687'), Rate.to_nominal(D('0.0375'), 12).round(5)
|
71
65
|
assert_equal D('0.03681'), Rate.to_nominal(D('0.0375'), Flt::DecNum.infinity).round(5)
|
72
66
|
end
|
73
67
|
|
74
|
-
should
|
68
|
+
it "should raise an exception if an unknown value is given for :type" do
|
75
69
|
assert_raises(ArgumentError){ Rate.new(0.0375, :foo) }
|
76
70
|
end
|
77
71
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finance
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-07-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: flt
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,33 +21,69 @@ dependencies:
|
|
21
21
|
version: 1.3.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.3.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 4.7.5
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 4.7.5
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activesupport
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 4.0.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 4.0.0
|
25
62
|
description: The finance library provides a Ruby interface for working with interest
|
26
63
|
rates, mortgage amortization, and cashflows (NPV, IRR, etc.).
|
27
64
|
email: wkranec@gmail.com
|
28
65
|
executables: []
|
29
66
|
extensions: []
|
30
67
|
extra_rdoc_files:
|
31
|
-
- README
|
68
|
+
- README.md
|
32
69
|
- COPYING
|
33
70
|
- COPYING.LESSER
|
34
71
|
- HISTORY
|
35
72
|
files:
|
36
|
-
- README
|
73
|
+
- README.md
|
37
74
|
- COPYING
|
38
75
|
- COPYING.LESSER
|
39
76
|
- HISTORY
|
77
|
+
- lib/finance.rb
|
78
|
+
- lib/finance/transaction.rb
|
40
79
|
- lib/finance/amortization.rb
|
41
|
-
- lib/finance/cashflows.rb
|
42
|
-
- lib/finance/decimal.rb
|
43
|
-
- lib/finance/interval.rb
|
44
80
|
- lib/finance/rates.rb
|
45
|
-
- lib/finance/
|
46
|
-
- lib/finance.rb
|
81
|
+
- lib/finance/decimal.rb
|
82
|
+
- lib/finance/cashflows.rb
|
47
83
|
- test/test_amortization.rb
|
48
84
|
- test/test_cashflows.rb
|
49
|
-
- test/test_interval.rb
|
50
85
|
- test/test_rates.rb
|
86
|
+
- test/test_helper.rb
|
51
87
|
homepage: https://rubygems.org/gems/finance
|
52
88
|
licenses: []
|
53
89
|
post_install_message:
|
@@ -68,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
104
|
version: '0'
|
69
105
|
requirements: []
|
70
106
|
rubyforge_project:
|
71
|
-
rubygems_version: 1.8.
|
107
|
+
rubygems_version: 1.8.24
|
72
108
|
signing_key:
|
73
109
|
specification_version: 3
|
74
110
|
summary: a library for financial modelling in Ruby.
|
data/lib/finance/interval.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
class Integer
|
2
|
-
# convert an integer value representing months (or years) into months
|
3
|
-
# @return [Integer] the number of months
|
4
|
-
# @example
|
5
|
-
# 360.months #=> 360
|
6
|
-
# 30.years #=> 360
|
7
|
-
# @api public
|
8
|
-
def method_missing(name, *args, &block)
|
9
|
-
return self if name.to_s == "months"
|
10
|
-
return self * 12 if name.to_s == "years"
|
11
|
-
super
|
12
|
-
end
|
13
|
-
end
|
data/test/test_interval.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require_relative '../lib/finance/interval.rb'
|
2
|
-
|
3
|
-
require 'minitest/unit'
|
4
|
-
require 'shoulda'
|
5
|
-
|
6
|
-
class TestInterval < Test::Unit::TestCase
|
7
|
-
context "a time interval" do
|
8
|
-
context "can be created from an integer" do
|
9
|
-
should "convert an integer into months" do
|
10
|
-
assert_equal 360, 360.months
|
11
|
-
end
|
12
|
-
|
13
|
-
should "convert an integer into years" do
|
14
|
-
assert_equal 360, 30.years
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|