finance 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +2 -0
- data/COPYING.LESSER +2 -0
- data/HISTORY +26 -0
- data/README +24 -22
- data/lib/finance.rb +5 -53
- data/lib/finance/amortization.rb +196 -0
- data/lib/finance/cashflows.rb +55 -0
- data/lib/finance/decimal.rb +12 -0
- data/lib/finance/interval.rb +19 -0
- data/lib/finance/rates.rb +167 -0
- data/lib/finance/transaction.rb +119 -0
- data/test/test_amortization.rb +114 -114
- data/test/test_cashflows.rb +12 -10
- data/test/test_interval.rb +18 -0
- data/test/test_rates.rb +69 -22
- metadata +19 -12
- data/lib/amortization.rb +0 -160
- data/lib/cashflows.rb +0 -33
- data/lib/rates.rb +0 -93
- data/lib/timespan.rb +0 -10
- data/test/test_timespan.rb +0 -12
data/COPYING
CHANGED
data/COPYING.LESSER
CHANGED
data/HISTORY
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
= Version 1.0.0
|
2
|
+
20 Jul 2011
|
3
|
+
|
4
|
+
* Moved to Ruby 1.9.
|
5
|
+
* All classes are now contained within the +Finance+ namespace.
|
6
|
+
* LOTS of additional documentation and examples.
|
7
|
+
* Introduced _shoulda_ for unit tests, to make things a little more readable.
|
8
|
+
* Bugfix: The +amortize+ Numeric method now accepts a variable number of rates.
|
9
|
+
* Some code refactoring and clean-up for a small performance increase.
|
10
|
+
|
11
|
+
= Version 0.2.0
|
12
|
+
28 Jun 2011
|
13
|
+
|
14
|
+
* Added support for adjustable rate mortgages.
|
15
|
+
* Added support for additional payments.
|
16
|
+
|
17
|
+
= Version 0.1.1
|
18
|
+
21 Jun 2011
|
19
|
+
|
20
|
+
* Code examples in README now display correctly in the online documentation.
|
21
|
+
|
22
|
+
= Version 0.1.0
|
23
|
+
21 Jun 2011
|
24
|
+
|
25
|
+
* Support for fixed-rate mortgage amortization.
|
26
|
+
* NPV, IRR array methods for cash flow analysis.
|
data/README
CHANGED
@@ -1,16 +1,25 @@
|
|
1
|
-
|
1
|
+
= FINANCE
|
2
2
|
|
3
|
-
|
3
|
+
a library for financial modelling in Ruby.
|
4
|
+
|
5
|
+
== INSTALL
|
4
6
|
|
5
7
|
$ sudo gem install finance
|
6
8
|
|
7
|
-
|
9
|
+
== OVERVIEW
|
8
10
|
|
9
|
-
|
11
|
+
=== GETTING STARTED
|
10
12
|
|
11
13
|
>> require 'finance'
|
12
14
|
|
13
|
-
|
15
|
+
*Note:* As of version 1.0.0, the entire library is contained under the
|
16
|
+
Finance namespace. Existing code will not work unless you add:
|
17
|
+
|
18
|
+
>> include Finance
|
19
|
+
|
20
|
+
for all of the examples below, we'll assume that you have done this.
|
21
|
+
|
22
|
+
=== AMORTIZATION
|
14
23
|
|
15
24
|
You are interested in borrowing $250,000 under a 30 year, fixed-rate
|
16
25
|
loan with a 4.25% APR.
|
@@ -55,7 +64,7 @@ Since we are looking at an ARM, there is no longer a single "payment" value.
|
|
55
64
|
But we can look at the different payments over time.
|
56
65
|
|
57
66
|
>> arm.payments.uniq
|
58
|
-
=> [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'),
|
67
|
+
=> [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), ... snipped ... ]
|
59
68
|
|
60
69
|
The other methods previously discussed can be accessed in the same way:
|
61
70
|
|
@@ -83,14 +92,11 @@ example. Notice the difference in the results:
|
|
83
92
|
>> extra_payments.interest.sum
|
84
93
|
=> DecNum('150566.24')
|
85
94
|
|
86
|
-
*Note*: you are _not_ allowed to modify a payment to pay _less_ than the
|
87
|
-
normally calculated payment.
|
88
|
-
|
89
95
|
You can also increase your payment to a specific amount:
|
90
96
|
|
91
97
|
>> extra_payments_2 = 250000.amortize(rate){ -1500 }
|
92
98
|
|
93
|
-
|
99
|
+
== ABOUT
|
94
100
|
|
95
101
|
I started developing _finance_ while analyzing mortgages as a personal
|
96
102
|
project. Spreadsheets have convenient formulas for doing this type of
|
@@ -110,29 +116,25 @@ of open, tested tools to fill this gap.
|
|
110
116
|
If you have used _finance_ and find it useful, I would enjoy hearing
|
111
117
|
about it!
|
112
118
|
|
113
|
-
|
119
|
+
== FEATURES
|
114
120
|
|
115
121
|
Currently implemented features include:
|
116
122
|
|
117
123
|
* Uses the {http://flt.rubyforge.org/ flt} library to ensure precision decimal arithmetic in all calculations.
|
118
124
|
* Fixed-rate mortgage amortization (30/360).
|
119
125
|
* Interest rates
|
120
|
-
* Various cash flow computations, such as NPV
|
126
|
+
* Various cash flow computations, such as NPV and IRR.
|
121
127
|
* Adjustable rate mortgage amortization.
|
122
128
|
* Payment modifications (i.e., how does paying an additional $75 per month affect the amortization?)
|
123
129
|
|
124
|
-
|
125
|
-
|
126
|
-
This gem and related documentation is available through
|
127
|
-
{https://rubygems.org/gems/finance RubyGems}.
|
128
|
-
|
129
|
-
Source code and bug tracking is available via
|
130
|
-
{http://github.com/wkranec/finance github}.
|
130
|
+
== RESOURCES
|
131
131
|
|
132
|
-
|
133
|
-
{
|
132
|
+
[RubyGems Page] {https://rubygems.org/gems/finance}
|
133
|
+
[Source Code] {http://github.com/wkranec/finance}
|
134
|
+
[Bug Tracker] {https://github.com/wkranec/finance/issues}
|
135
|
+
[Google Group] {http://groups.google.com/group/finance-gem/topics?pli=1}
|
134
136
|
|
135
|
-
|
137
|
+
== COPYRIGHT
|
136
138
|
|
137
139
|
This library is released under the terms of the LGPL license.
|
138
140
|
|
data/lib/finance.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'rates'
|
4
|
-
require 'timespan'
|
1
|
+
require 'finance/cashflows'
|
2
|
+
require 'finance/interval'
|
5
3
|
|
6
4
|
# The *Finance* module adheres to the following conventions for
|
7
5
|
# financial calculations:
|
@@ -11,53 +9,7 @@ require 'timespan'
|
|
11
9
|
# * *principal* represents the outstanding balance of a loan or annuity.
|
12
10
|
# * *rate* represents the interest rate _per period_.
|
13
11
|
module Finance
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
12
|
+
autoload :Amortization, 'finance/amortization'
|
13
|
+
autoload :Rate, 'finance/rates'
|
14
|
+
autoload :Transaction, 'finance/transaction'
|
63
15
|
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require_relative 'cashflows'
|
2
|
+
require_relative 'decimal'
|
3
|
+
require_relative 'transaction'
|
4
|
+
|
5
|
+
module Finance
|
6
|
+
# the Amortization class provides an interface for working with loan amortizations.
|
7
|
+
# @note There are _two_ ways to create an amortization. The first
|
8
|
+
# example uses the amortize method for the Numeric class. The second
|
9
|
+
# calls Amortization.new directly.
|
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.years)
|
12
|
+
# amortization = 250000.amortize(rate)
|
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
|
+
# 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.years ) }
|
16
|
+
# arm = Amortization.new(250000, *rates)
|
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 => 30.years)
|
19
|
+
# extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
|
20
|
+
# @api public
|
21
|
+
class Amortization
|
22
|
+
# @return [DecNum] the balance of the loan at the end of the amortization period (usually zero)
|
23
|
+
# @api public
|
24
|
+
attr_reader :balance
|
25
|
+
# @return [DecNum] the required monthly payment. For loans with more than one rate, returns nil
|
26
|
+
# @api public
|
27
|
+
attr_reader :payment
|
28
|
+
# @return [DecNum] the principal amount of the loan
|
29
|
+
# @api public
|
30
|
+
attr_reader :principal
|
31
|
+
# @return [Array] the interest rates used for calculating the amortization
|
32
|
+
# @api public
|
33
|
+
attr_reader :rates
|
34
|
+
|
35
|
+
# compare two Amortization instances
|
36
|
+
# @return [Numeric] -1, 0, or +1
|
37
|
+
# @param [Amortization]
|
38
|
+
# @api public
|
39
|
+
def ==(amortization)
|
40
|
+
self.principal == amortization.principal and self.rates == amortization.rates and self.payments == amortization.payments
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Array] the amount of any additional payments in each period
|
44
|
+
# @example
|
45
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
46
|
+
# amt = 300000.amortize(rate){ |payment| payment.amount-100}
|
47
|
+
# amt.additional_payments #=> [DecNum('-100.00'), DecNum('-100.00'), ... ]
|
48
|
+
# @api public
|
49
|
+
def additional_payments
|
50
|
+
@transactions.find_all(&:payment?).collect{ |p| p.difference }
|
51
|
+
end
|
52
|
+
|
53
|
+
# amortize the balance of loan with the given interest rate
|
54
|
+
# @return none
|
55
|
+
# @param [Rate] rate the interest rate to use in the amortization
|
56
|
+
# @api private
|
57
|
+
def amortize(rate)
|
58
|
+
# For the purposes of calculating a payment, the relevant time
|
59
|
+
# period is the remaining number of periods in the loan, not
|
60
|
+
# necessarily the duration of the rate itself.
|
61
|
+
periods = @periods - @period
|
62
|
+
amount = Amortization.payment @balance, rate.monthly, periods
|
63
|
+
|
64
|
+
pmt = Payment.new(amount, :period => @period)
|
65
|
+
if @block then pmt.modify(&@block) end
|
66
|
+
|
67
|
+
rate.duration.times do
|
68
|
+
# Do this first in case the balance is zero already.
|
69
|
+
if @balance.zero? then break end
|
70
|
+
|
71
|
+
# Compute and record interest on the outstanding balance.
|
72
|
+
int = (@balance * rate.monthly).round(2)
|
73
|
+
interest = Interest.new(int, :period => @period)
|
74
|
+
@balance += interest.amount
|
75
|
+
@transactions << interest.dup
|
76
|
+
|
77
|
+
# Record payment. Don't pay more than the outstanding balance.
|
78
|
+
if pmt.amount.abs > @balance then pmt.amount = -@balance end
|
79
|
+
@transactions << pmt.dup
|
80
|
+
@balance += pmt.amount
|
81
|
+
|
82
|
+
@period += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# compute the amortization of the principal
|
87
|
+
# @return none
|
88
|
+
# @api private
|
89
|
+
def compute
|
90
|
+
@balance = @principal
|
91
|
+
@transactions = []
|
92
|
+
|
93
|
+
@rates.each do |rate|
|
94
|
+
amortize(rate)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Add any remaining balance due to rounding error to the last payment.
|
98
|
+
unless @balance.zero?
|
99
|
+
@transactions.find_all(&:payment?)[-1].amount -= @balance
|
100
|
+
@balance = 0
|
101
|
+
end
|
102
|
+
|
103
|
+
if @rates.length == 1
|
104
|
+
@payment = self.payments[0]
|
105
|
+
else
|
106
|
+
@payment = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
@transactions.freeze
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Integer] the time required to pay off the loan, in months
|
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.years)
|
115
|
+
# amt = 300000.amortize(rate)
|
116
|
+
# amt.duration #=> 360
|
117
|
+
# @example Extra payments may reduce the duration
|
118
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
119
|
+
# amt = 300000.amortize(rate){ |payment| payment.amount-100}
|
120
|
+
# amt.duration #=> 319
|
121
|
+
# @api public
|
122
|
+
def duration
|
123
|
+
self.payments.length
|
124
|
+
end
|
125
|
+
|
126
|
+
# create a new Amortization instance
|
127
|
+
# @return [Amortization]
|
128
|
+
# @param [DecNum] principal the initial amount of the loan or investment
|
129
|
+
# @param [Rate] rates the applicable interest rates
|
130
|
+
# @param [Proc] block
|
131
|
+
# @api public
|
132
|
+
def initialize(principal, *rates, &block)
|
133
|
+
@principal = principal.to_d
|
134
|
+
@rates = rates
|
135
|
+
@block = block
|
136
|
+
|
137
|
+
# compute the total duration from all of the rates.
|
138
|
+
@periods = (rates.collect { |r| r.duration }).sum
|
139
|
+
@period = 0
|
140
|
+
|
141
|
+
compute
|
142
|
+
end
|
143
|
+
|
144
|
+
# @api public
|
145
|
+
def inspect
|
146
|
+
"Amortization.new(#{@principal})"
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Array] the amount of interest charged in each period
|
150
|
+
# @example find the total cost of interest for a loan
|
151
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
152
|
+
# amt = 300000.amortize(rate)
|
153
|
+
# amt.interest.sum #=> DecNum('200163.94')
|
154
|
+
# @example find the total interest charges in the first six months
|
155
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
156
|
+
# amt = 300000.amortize(rate)
|
157
|
+
# amt.interest[0,6].sum #=> DecNum('5603.74')
|
158
|
+
# @api public
|
159
|
+
def interest
|
160
|
+
@transactions.find_all(&:interest?).collect{ |p| p.amount }
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [DecNum] the periodic payment due on a loan
|
164
|
+
# @param [DecNum] principal the initial amount of the loan or investment
|
165
|
+
# @param [Rate] rate the applicable interest rate (per period)
|
166
|
+
# @param [Integer] periods the number of periods needed for repayment
|
167
|
+
# @note in most cases, you will probably want to use rate.monthly when calling this function outside of an Amortization instance.
|
168
|
+
# @example
|
169
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
170
|
+
# rate.duration #=> 360
|
171
|
+
# Amortization.payment(200000, rate.monthly, rate.duration) #=> DecNum('-926.23')
|
172
|
+
# @see http://en.wikipedia.org/wiki/Amortization_calculator
|
173
|
+
# @api public
|
174
|
+
def Amortization.payment(principal, rate, periods)
|
175
|
+
-(principal * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [Array] the amount of the payment in each period
|
179
|
+
# @example find the total payments for a loan
|
180
|
+
# rate = Rate.new(0.0375, :apr, :duration => 30.years)
|
181
|
+
# amt = 300000.amortize(rate)
|
182
|
+
# amt.payments.sum #=> DecNum('-500163.94')
|
183
|
+
# @api public
|
184
|
+
def payments
|
185
|
+
@transactions.find_all(&:payment?).collect{ |p| p.amount }
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class Numeric
|
191
|
+
# @see Amortization#new
|
192
|
+
# @api public
|
193
|
+
def amortize(*rates, &block)
|
194
|
+
amortization = Amortization.new(self, *rates, &block)
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative 'decimal'
|
2
|
+
|
3
|
+
module Finance
|
4
|
+
# Provides methods for working with cash flows (collections of transactions)
|
5
|
+
# @api public
|
6
|
+
module Cashflow
|
7
|
+
# calculate the internal rate of return for a sequence of cash flows
|
8
|
+
# @return [DecNum] the internal rate of return
|
9
|
+
# @example
|
10
|
+
# [-4000,1200,1410,1875,1050].irr #=> 0.143
|
11
|
+
# @see http://en.wikipedia.org/wiki/Internal_rate_of_return
|
12
|
+
# @api public
|
13
|
+
def irr(iterations=100)
|
14
|
+
self.collect! { |entry| entry.to_d }
|
15
|
+
|
16
|
+
rate, investment = 1.to_d, self[0]
|
17
|
+
iterations.times do
|
18
|
+
rate *= (1 - self.npv(rate) / investment)
|
19
|
+
end
|
20
|
+
|
21
|
+
rate
|
22
|
+
end
|
23
|
+
|
24
|
+
# calculate the net present value of a sequence of cash flows
|
25
|
+
# @return [DecNum] the net present value
|
26
|
+
# @param [Numeric] rate the discount rate to be applied
|
27
|
+
# @example
|
28
|
+
# [-100.0, 60, 60, 60].npv(0.1) #=> 49.211
|
29
|
+
# @see http://en.wikipedia.org/wiki/Net_present_value
|
30
|
+
# @api public
|
31
|
+
def npv(rate)
|
32
|
+
self.collect! { |entry| entry.to_d }
|
33
|
+
|
34
|
+
rate, total = rate.to_d, 0.to_d
|
35
|
+
self.each_with_index do |cashflow, index|
|
36
|
+
total += cashflow / (1 + rate) ** index
|
37
|
+
end
|
38
|
+
|
39
|
+
total
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Numeric] the total value of a sequence of cash flows
|
43
|
+
# @api public
|
44
|
+
def sum
|
45
|
+
self.inject(:+)
|
46
|
+
end
|
47
|
+
|
48
|
+
def xirr
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Array
|
54
|
+
include Finance::Cashflow
|
55
|
+
end
|