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 CHANGED
@@ -1,3 +1,5 @@
1
+ # @title COPYING
2
+ # @markup none
1
3
  GNU GENERAL PUBLIC LICENSE
2
4
  Version 3, 29 June 2007
3
5
 
@@ -1,3 +1,5 @@
1
+ # @title COPYING.LESSER
2
+ # @markup none
1
3
  GNU LESSER GENERAL PUBLIC LICENSE
2
4
  Version 3, 29 June 2007
3
5
 
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
- _finance_ - a library for financial calculations in Ruby.
1
+ = FINANCE
2
2
 
3
- = INSTALL
3
+ a library for financial modelling in Ruby.
4
+
5
+ == INSTALL
4
6
 
5
7
  $ sudo gem install finance
6
8
 
7
- = OVERVIEW
9
+ == OVERVIEW
8
10
 
9
- == GETTING STARTED
11
+ === GETTING STARTED
10
12
 
11
13
  >> require 'finance'
12
14
 
13
- == AMORTIZATION
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'), DecNum('-1641.34'), ... snipped ... ]
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
- = ABOUT
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
- = FEATURES
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, IRR, and sum.
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
- = RESOURCES
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
- Additional documentation is available on the
133
- {https://github.com/wkranec/finance/wiki wiki}.
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
- = COPYRIGHT
137
+ == COPYRIGHT
136
138
 
137
139
  This library is released under the terms of the LGPL license.
138
140
 
@@ -1,7 +1,5 @@
1
- require 'amortization'
2
- require 'cashflows'
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
- 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
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