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.
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'flt'
3
+
4
+ class Numeric
5
+ def to_d
6
+ if self.instance_of? Flt::DecNum
7
+ self
8
+ else
9
+ Flt::DecNum self.to_s
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ class Integer
2
+ # convert an integer value representing months into months
3
+ # @return [Integer] the number of months
4
+ # @example
5
+ # 360.months #=> 360
6
+ # @api public
7
+ def months
8
+ self
9
+ end
10
+
11
+ # convert an integer value representing years into months
12
+ # @return [Integer] the number of months
13
+ # @example
14
+ # 30.years #=> 360
15
+ # @api public
16
+ def years
17
+ self * 12
18
+ end
19
+ end
@@ -0,0 +1,167 @@
1
+ require_relative 'decimal'
2
+
3
+ module Finance
4
+ # the Rate class provides an interface for working with interest rates.
5
+ # {render:Rate#new}
6
+ # @api public
7
+ class Rate
8
+ include Comparable
9
+
10
+ # Accepted rate types
11
+ TYPES = { :apr => "effective",
12
+ :apy => "effective",
13
+ :effective => "effective",
14
+ :nominal => "nominal"
15
+ }
16
+
17
+ # @return [Integer] the duration for which the rate is valid, in months
18
+ # @api public
19
+ attr_accessor :duration
20
+ # @return [DecNum] the effective interest rate
21
+ # @api public
22
+ attr_reader :effective
23
+ # @return [DecNum] the nominal interest rate
24
+ # @api public
25
+ attr_reader :nominal
26
+
27
+ # compare two Rates, using the effective rate
28
+ # @return [Numeric] one of -1, 0, +1
29
+ # @param [Rate] rate the comparison Rate
30
+ # @example Which is better, a nominal rate of 15% compounded monthly, or 15.5% compounded semiannually?
31
+ # r1 = Rate.new(0.15, :nominal) #=> Rate.new(0.160755, :apr)
32
+ # r2 = Rate.new(0.155, :nominal, :compounds => :semiannually) #=> Rate.new(0.161006, :apr)
33
+ # r1 <=> r2 #=> -1
34
+ # @api public
35
+ def <=>(rate)
36
+ @effective <=> rate.effective
37
+ end
38
+
39
+ # (see #effective)
40
+ # @api public
41
+ def apr
42
+ self.effective
43
+ end
44
+
45
+ # (see #effective)
46
+ # @api public
47
+ def apy
48
+ self.effective
49
+ end
50
+
51
+ # a convenience method which sets the value of @periods
52
+ # @return none
53
+ # @param [Symbol, Numeric] input the compounding frequency
54
+ # @raise [ArgumentError] if input is not an accepted keyword or Numeric
55
+ # @api private
56
+ def compounds=(input)
57
+ @periods = case input
58
+ when :annually then Flt::DecNum 1
59
+ when :continuously then Flt::DecNum.infinity
60
+ when :daily then Flt::DecNum 365
61
+ when :monthly then Flt::DecNum 12
62
+ when :quarterly then Flt::DecNum 4
63
+ when :semiannually then Flt::DecNum 2
64
+ when Numeric then Flt::DecNum input.to_s
65
+ else raise ArgumentError
66
+ end
67
+ end
68
+
69
+ # set the effective interest rate
70
+ # @return none
71
+ # @param [DecNum] rate the effective interest rate
72
+ # @api private
73
+ def effective=(rate)
74
+ @effective = rate
75
+ @nominal = Rate.to_nominal(rate, @periods)
76
+ end
77
+
78
+ # create a new Rate instance
79
+ # @return [Rate]
80
+ # @param [Numeric] rate the decimal value of the interest rate
81
+ # @param [Symbol] type a valid {TYPES rate type}
82
+ # @param [optional, Hash] opts set optional attributes
83
+ # @option opts [String] :duration a time interval for which the rate is valid
84
+ # @option opts [String] :compounds (:monthly) the number of compounding periods per year
85
+ # @example create a 3.5% APR rate
86
+ # Rate.new(0.035, :apr) #=> Rate(0.035, :apr)
87
+ # @see http://en.wikipedia.org/wiki/Effective_interest_rate
88
+ # @see http://en.wikipedia.org/wiki/Nominal_interest_rate
89
+ # @api public
90
+ def initialize(rate, type, opts={})
91
+ # Default monthly compounding.
92
+ opts = { :compounds => :monthly }.merge opts
93
+
94
+ # Set optional attributes..
95
+ opts.each do |key, value|
96
+ send("#{key}=", value)
97
+ end
98
+
99
+ # Set the rate in the proper way, based on the value of type.
100
+ begin
101
+ send("#{TYPES.fetch(type)}=", rate.to_d)
102
+ rescue KeyError
103
+ raise ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller
104
+ end
105
+ end
106
+
107
+ def inspect
108
+ "Rate.new(#{self.apr.round(6)}, :apr)"
109
+ end
110
+
111
+ # @return [DecNum] the monthly effective interest rate
112
+ # @example
113
+ # rate = Rate.new(0.15, :nominal)
114
+ # rate.apr.round(6) #=> DecNum('0.160755')
115
+ # rate.monthly.round(6) #=> DecNum('0.013396')
116
+ # @api public
117
+ def monthly
118
+ (self.effective / 12).round(15)
119
+ end
120
+
121
+ # set the nominal interest rate
122
+ # @return none
123
+ # @param [DecNum] rate the nominal interest rate
124
+ # @api private
125
+ def nominal=(rate)
126
+ @nominal = rate
127
+ @effective = Rate.to_effective(rate, @periods)
128
+ end
129
+
130
+ # convert a nominal interest rate to an effective interest rate
131
+ # @return [DecNum] the effective interest rate
132
+ # @param [Numeric] rate the nominal interest rate
133
+ # @param [Numeric] periods the number of compounding periods per year
134
+ # @example
135
+ # Rate.to_effective(0.05, 4) #=> DecNum('0.05095')
136
+ # @api public
137
+ def Rate.to_effective(rate, periods)
138
+ rate, periods = rate.to_d, periods.to_d
139
+
140
+ if periods.infinite?
141
+ rate.exp - 1
142
+ else
143
+ (1 + rate / periods) ** periods - 1
144
+ end
145
+ end
146
+
147
+ # convert an effective interest rate to a nominal interest rate
148
+ # @return [DecNum] the nominal interest rate
149
+ # @param [Numeric] rate the effective interest rate
150
+ # @param [Numeric] periods the number of compounding periods per year
151
+ # @example
152
+ # Rate.to_nominal(0.06, 365) #=> DecNum('0.05827')
153
+ # @see http://www.miniwebtool.com/nominal-interest-rate-calculator/
154
+ # @api public
155
+ def Rate.to_nominal(rate, periods)
156
+ rate, periods = rate.to_d, periods.to_d
157
+
158
+ if periods.infinite?
159
+ (rate + 1).log
160
+ else
161
+ periods * ((1 + rate) ** (1 / periods) - 1)
162
+ end
163
+ end
164
+
165
+ private :compounds=, :effective=, :nominal=
166
+ end
167
+ end
@@ -0,0 +1,119 @@
1
+ require_relative 'decimal'
2
+
3
+ module Finance
4
+ # the Transaction class provides a general interface for working with individual cash flows.
5
+ # @api public
6
+ class Transaction
7
+ # @return [DecNum] the cash value of the transaction
8
+ # @api public
9
+ attr_reader :amount
10
+ # @return [Integer] the period number of the transaction
11
+ # @note this attribute is mainly used in the case of mortgage amortization with no dates
12
+ # @api public
13
+ attr_accessor :period
14
+
15
+ # Set the cash value of the transaction
16
+ # @return None
17
+ # @param [Numeric] value the cash value
18
+ # @example
19
+ # t = Transaction.new(500)
20
+ # t.amount = 750
21
+ # t.amount #=> 750
22
+ # @api public
23
+ def amount=(value)
24
+ @amount = value.to_d
25
+ end
26
+
27
+ # @return [DecNum] the difference between the original transaction
28
+ # amount and the current amount
29
+ # @example
30
+ # t = Transaction.new(500)
31
+ # t.amount = 750
32
+ # t.difference #=> DecNum('250')
33
+ # @api public
34
+ def difference
35
+ @amount - @original
36
+ end
37
+
38
+ # create a new Transaction
39
+ # @return [Transaction]
40
+ # @param [Numeric] amount the cash value of the transaction
41
+ # @param [optional, Hash] opts sets optional attributes
42
+ # @option opts [String] :period the period number of the transaction
43
+ # @example a simple transaction
44
+ # t = Transaction.new(400)
45
+ # @example a transaction with a period number
46
+ # t = Transaction.new(400, :period => 3)
47
+ # @api public
48
+ def initialize(amount, opts={})
49
+ @amount = amount
50
+ @original = amount
51
+
52
+ # Set optional attributes..
53
+ opts.each do |key, value|
54
+ send("#{key}=", value)
55
+ end
56
+ end
57
+
58
+ # @return [Boolean] whether or not the Transaction is an Interest transaction
59
+ # @example
60
+ # pmt = Payment.new(500)
61
+ # int = Interest.new(500)
62
+ # pmt.interest? #=> False
63
+ # int.interest? #=> True
64
+ # @api public
65
+ def interest?
66
+ self.instance_of? Interest
67
+ end
68
+
69
+ # @api public
70
+ def inspect
71
+ "Transaction(#{@amount})"
72
+ end
73
+
74
+ # Modify a Transaction's amount by passing a block
75
+ # @return none
76
+ # @note self is passed as the argument to the block. This makes any public attribute available.
77
+ # @example add $100 to a monthly payment
78
+ # pmt = Payment.new(-500)
79
+ # pmt.modify { |t| t.amount-100 }
80
+ # pmt.amount #=> -600
81
+ # @api public
82
+ def modify
83
+ @amount = yield(self)
84
+ end
85
+
86
+ # (see #amount)
87
+ # @deprecated Provided for backwards compatibility
88
+ def payment
89
+ @amount
90
+ end
91
+
92
+ # @return [Boolean] whether or not the Transaction is a Payment transaction
93
+ # @example
94
+ # pmt = Payment.new(500)
95
+ # int = Interest.new(500)
96
+ # pmt.payment? #=> True
97
+ # int.payment? #=> False
98
+ # @api public
99
+ def payment?
100
+ self.instance_of? Payment
101
+ end
102
+ end
103
+
104
+ # Represent an interest charge as a Transaction
105
+ # @see Transaction
106
+ class Interest < Transaction
107
+ def inspect
108
+ "Interest(#{@amount})"
109
+ end
110
+ end
111
+
112
+ # Represent a loan payment as a Transaction
113
+ # @see Transaction
114
+ class Payment < Transaction
115
+ def inspect
116
+ "Payment(#{@amount})"
117
+ end
118
+ end
119
+ end
@@ -1,156 +1,156 @@
1
- require 'rubygems'
2
- require 'finance'
1
+ require_relative '../lib/finance/amortization.rb'
2
+ require_relative '../lib/finance/interval.rb'
3
+ require_relative '../lib/finance/rates.rb'
4
+ include Finance
5
+
3
6
  require 'flt/d'
4
- require 'test/unit'
7
+ require 'minitest/unit'
8
+ require 'shoulda'
5
9
 
6
- class TestBasicAmortization < Test::Unit::TestCase
7
- def interest_in_period(principal, rate, payment, period)
10
+ # @see http://tinyurl.com/6zroqvd for detailed calculations for the
11
+ # examples in these unit tests.
12
+ class TestAmortization < Test::Unit::TestCase
13
+ def ipmt(principal, rate, payment, period)
8
14
  -(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
9
15
  end
10
16
 
11
- def setup
12
- @rate = Rate.new(0.0375, :apr, :duration => 30.years)
13
- @principal = D(200000)
14
- @amortization = Amortization.new(@principal, @rate)
15
- end
17
+ context "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
18
+ setup do
19
+ @rate = Rate.new(0.0375, :apr, :duration => 30.years)
20
+ @principal = D(200000)
21
+ @std = Amortization.new(@principal, @rate)
22
+ end
16
23
 
17
- def test_balance
18
- assert @amortization.balance.zero?
19
- end
24
+ should "have a principal of $200,000" do
25
+ assert_equal @principal, @std.principal
26
+ end
20
27
 
21
- def test_duration
22
- assert_equal 360, @amortization.duration
23
- end
28
+ should "have a final balance of zero" do
29
+ assert @std.balance.zero?
30
+ end
24
31
 
25
- def test_interest
26
- 0.upto 359 do |period|
27
- assert_equal @amortization.interest[period], interest_in_period(@principal, @rate.monthly, @amortization.payment, period+1)
32
+ should "have a duration of 360 months" do
33
+ assert_equal 360, @std.duration
28
34
  end
29
- end
30
35
 
31
- def test_interest_sum
32
- assert_equal D('133443.53'), @amortization.interest.sum
33
- end
36
+ should "have a monthly payment of $926.23" do
37
+ assert_equal D('-926.23'), @std.payment
38
+ end
34
39
 
35
- def test_payment
36
- assert_equal D('-926.23'), @amortization.payment
37
- end
40
+ should "have a final payment of $926.96 (due to rounding)" do
41
+ assert_equal D('-926.96'), @std.payments[-1]
42
+ end
38
43
 
39
- def test_payments
40
- payments = [ D('-926.23') ] * @rate.duration
41
- # Account for rounding errors in last payment.
42
- payments[-1] = D('-926.96')
43
- assert_equal payments, @amortization.payments
44
- end
44
+ should "have total payments of $333,443.53" do
45
+ assert_equal D('-333443.53'), @std.payments.sum
46
+ end
45
47
 
46
- def test_payments_sum
47
- assert_equal D('-333443.53'), @amortization.payments.sum
48
- end
48
+ should "have interest charges which agree with the standard formula" do
49
+ 0.upto 359 do |period|
50
+ assert_equal @std.interest[period], ipmt(@principal, @rate.monthly, @std.payment, period+1)
51
+ end
52
+ end
49
53
 
50
- def test_principal
51
- assert_equal @principal, @amortization.principal
54
+ should "have total interest charges of $133,433.33" do
55
+ assert_equal D('133443.53'), @std.interest.sum
56
+ end
52
57
  end
53
58
 
54
- def test_sum
55
- assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
56
- end
57
- end
59
+ context "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
60
+ setup do
61
+ @rates = []
62
+ 0.upto 9 do |adj|
63
+ @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3.years)
64
+ end
65
+ @principal = D(200000)
66
+ @arm = Amortization.new(@principal, *@rates)
67
+ end
58
68
 
59
- class TestAdjustableAmortization < Test::Unit::TestCase
60
- def setup
61
- @rates = []
62
- 0.upto 9 do |adj|
63
- @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3.years)
69
+ should "have a principal of $200,000" do
70
+ assert_equal @principal, @arm.principal
64
71
  end
65
- @principal = D(200000)
66
- @amortization = Amortization.new(@principal, *@rates)
67
- end
68
72
 
69
- def test_balance
70
- assert @amortization.balance.zero?
71
- end
73
+ should "have a final balance of zero" do
74
+ assert @arm.balance.zero?
75
+ end
72
76
 
73
- def test_duration
74
- assert_equal 360, @amortization.duration
75
- end
77
+ should "have a duration of 360 months" do
78
+ assert_equal 360, @arm.duration
79
+ end
76
80
 
77
- def test_interest_sum
78
- assert_equal D('277505.92'), @amortization.interest.sum
79
- end
81
+ should "not have a fixed monthly payment (since it changes)" do
82
+ assert_nil @arm.payment
83
+ end
80
84
 
81
- def test_payment
82
- assert_nil @amortization.payment
83
- end
85
+ should "have payments which increase every three years" do
86
+ values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 }
87
+ values.collect!{ |v| -D(v) }
88
+
89
+ payments = []
90
+ values[0,9].each do |v|
91
+ 36.times do
92
+ payments << v
93
+ end
94
+ end
95
+
96
+ 35.times { payments << values[9] }
84
97
 
85
- def test_payments
86
- values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 1601.78}
87
- values.collect!{ |v| -D(v) }
88
-
89
- payments = []
90
- values[0,9].each do |v|
91
- 36.times do
92
- payments << v
98
+ payments[0..-2].each_with_index do |payment, index|
99
+ assert_equal payment, @arm.payments[index]
93
100
  end
94
101
  end
95
102
 
96
- 35.times { payments << values[9] }
97
- payments << values[10]
98
-
99
- payments.each_with_index do |payment, index|
100
- assert_equal payment, @amortization.payments[index]
103
+ should "have a final payment of $1601.78 (due to rounding)" do
104
+ assert_equal D('-1601.78'), @arm.payments[-1]
101
105
  end
102
- end
103
106
 
104
- def test_payment_sum
105
- assert_equal D('-477505.92'), @amortization.payments.sum
106
- end
107
+ should "have total payments of $47,505.92" do
108
+ assert_equal D('-477505.92'), @arm.payments.sum
109
+ end
107
110
 
108
- def test_principal
109
- assert_equal @principal, @amortization.principal
111
+ should "have total interest charges of $277,505.92" do
112
+ assert_equal D('277505.92'), @arm.interest.sum
113
+ end
110
114
  end
111
115
 
112
- def test_sum
113
- assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
114
- end
115
- end
116
+ context "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
117
+ setup do
118
+ @rate = Rate.new(0.0375, :apr, :duration => 30.years)
119
+ @principal = D(200000)
120
+ @exp = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
121
+ end
116
122
 
117
- class TestExtraPaymentAmortization < Test::Unit::TestCase
118
- def setup
119
- @rate = Rate.new(0.0375, :apr, :duration => 30.years)
120
- @principal = D(200000)
121
- @amortization = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
122
- end
123
+ should "have a principal of $200,000" do
124
+ assert_equal @principal, @exp.principal
125
+ end
123
126
 
124
- def test_additional_payments_sum
125
- assert_equal D('-30084.86'), @amortization.additional_payments.sum
126
- end
127
-
128
- def test_balance
129
- assert @amortization.balance.zero?
130
- end
127
+ should "have a final balance of zero" do
128
+ assert @exp.balance.zero?
129
+ end
131
130
 
132
- def test_duration
133
- assert_equal 301, @amortization.duration
134
- end
131
+ should "have a duration of 301 months" do
132
+ assert_equal 301, @exp.duration
133
+ end
135
134
 
136
- def test_interest_sum
137
- assert_equal D('108880.09'), @amortization.interest.sum
138
- end
135
+ should "have a monthly payment of $1026.23" do
136
+ assert_equal D('-1026.23'), @exp.payment
137
+ end
139
138
 
140
- def test_payment
141
- assert_equal D('-1026.23'), @amortization.payment
142
- end
139
+ should "have a final payment of $1011.09" do
140
+ assert_equal D('-1011.09'), @exp.payments[-1]
141
+ end
143
142
 
144
- def test_payment_sum
145
- assert_equal D('-308880.09'), @amortization.payments.sum
146
- end
143
+ should "have total payments of $308,880.09" do
144
+ assert_equal D('-308880.09'), @exp.payments.sum
145
+ end
147
146
 
148
- def test_principal
149
- assert_equal @principal, @amortization.principal
150
- end
147
+ should "have total additional payments of $30,084.86" do
148
+ assert_equal D('-30084.86'), @exp.additional_payments.sum
149
+ end
151
150
 
152
- def test_sum
153
- assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
151
+ should "have total interest charges of $108880.09" do
152
+ assert_equal D('108880.09'), @exp.interest.sum
153
+ end
154
154
  end
155
155
  end
156
156