finance 0.2.0 → 1.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/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
|