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 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