finance 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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