finance 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,16 @@
1
- require 'cashflows'
2
- require 'test/unit'
1
+ require_relative '../lib/finance/cashflows.rb'
3
2
 
4
- class TestIRR < Test::Unit::TestCase
5
- def test_simple
6
- assert_in_delta(0.143, [-4000,1200,1410,1875,1050].irr, 0.001)
7
- end
8
- end
3
+ require 'flt/d'
4
+ require 'minitest/unit'
5
+ require 'shoulda'
9
6
 
10
- class TestNPV < Test::Unit::TestCase
11
- def test_simple
12
- assert_in_delta(49.211, [-100.0, 60, 60, 60].npv(0.1), 0.001)
7
+ class TestCashflows < Test::Unit::TestCase
8
+ context "an array of cashflows" do
9
+ should "have an Internal Rate of Return" do
10
+ assert_equal D("0.143"), [-4000,1200,1410,1875,1050].irr.round(3)
11
+ end
12
+ should "have a Net Present Value" do
13
+ assert_equal D("49.211"), [-100.0, 60, 60, 60].npv(0.1).round(3)
14
+ end
13
15
  end
14
16
  end
@@ -0,0 +1,18 @@
1
+ require_relative '../lib/finance/interval.rb'
2
+
3
+ require 'minitest/unit'
4
+ require 'shoulda'
5
+
6
+ class TestInterval < Test::Unit::TestCase
7
+ context "a time interval" do
8
+ context "can be created from an integer" do
9
+ should "convert an integer into months" do
10
+ assert_equal 360, 360.months
11
+ end
12
+
13
+ should "convert an integer into years" do
14
+ assert_equal 360, 30.years
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,31 +1,78 @@
1
- require 'rubygems'
2
- require 'finance'
1
+ require_relative '../lib/finance/rates.rb'
2
+ include Finance
3
+
4
+ require 'flt'
3
5
  require 'flt/d'
4
- require 'test/unit'
6
+ require 'minitest/unit'
7
+ require 'shoulda'
5
8
 
6
9
  class TestRates < Test::Unit::TestCase
7
- def test_apr
8
- rate = Rate.new(0.0375, :apr)
9
- assert_equal D('0.03687'), rate.nominal.round(5)
10
- end
10
+ context "an interest rate" do
11
+ context "can compound with different periods" do
12
+ should "compound monthly by default" do
13
+ rate = Rate.new(0.15, :nominal)
14
+ assert_equal D('0.16075'), rate.effective.round(5)
15
+ end
11
16
 
12
- def test_duration
13
- rate = Rate.new(0.0375, :effective, :duration => 30.years)
14
- assert_equal 360, rate.duration
15
- end
17
+ should "compound annually" do
18
+ rate = Rate.new(0.15, :nominal, :compounds => :annually)
19
+ assert_equal D('0.15'), rate.effective
20
+ end
21
+
22
+ should "compound continuously" do
23
+ rate = Rate.new(0.15, :nominal, :compounds => :continuously)
24
+ assert_equal D('0.16183'), rate.effective.round(5)
25
+ end
26
+
27
+ should "compound daily" do
28
+ rate = Rate.new(0.15, :nominal, :compounds => :daily)
29
+ assert_equal D('0.16180'), rate.effective.round(5)
30
+ end
31
+
32
+ should "compound quarterly" do
33
+ rate = Rate.new(0.15, :nominal, :compounds => :quarterly)
34
+ assert_equal D('0.15865'), rate.effective.round(5)
35
+ end
36
+
37
+ should "compound semiannually" do
38
+ rate = Rate.new(0.15, :nominal, :compounds => :semiannually)
39
+ assert_equal D('0.15563'), rate.effective.round(5)
40
+ end
41
+
42
+ should "accept a numerical value as the compounding frequency per year" do
43
+ rate = Rate.new(0.15, :nominal, :compounds => 7)
44
+ assert_equal D('0.15999'), rate.effective.round(5)
45
+ end
46
+
47
+ should "raise an exception if an unknown string is given" do
48
+ assert_raises(ArgumentError){ Rate.new(0.15, :nominal, :compounds => :quickly) }
49
+ end
50
+ end
51
+
52
+ should "accept a duration if given" do
53
+ rate = Rate.new(0.0375, :effective, :duration => 360)
54
+ assert_equal 360, rate.duration
55
+ end
56
+
57
+ should "be comparable to other interest rates" do
58
+ r1 = Rate.new(0.15, :nominal)
59
+ r2 = Rate.new(0.16, :nominal)
60
+ assert_equal( 1, r2 <=> r1)
61
+ assert_equal(-1, r1 <=> r2)
62
+ end
16
63
 
17
- def test_effective
18
- rate = Rate.new(0.03687, :nominal)
19
- assert_equal D('0.0375'), rate.effective.round(4)
20
- end
64
+ should "convert to a monthly value" do
65
+ rate = Rate.new(0.0375, :effective)
66
+ assert_equal D('0.003125'), rate.monthly
67
+ end
21
68
 
22
- def test_monthly
23
- rate = Rate.new(0.0375, :effective)
24
- assert_equal D('0.003125'), rate.monthly
25
- end
69
+ should "convert effective interest rates to nominal" do
70
+ assert_equal D('0.03687'), Rate.to_nominal(D('0.0375'), 12).round(5)
71
+ assert_equal D('0.03681'), Rate.to_nominal(D('0.0375'), Flt::DecNum.infinity).round(5)
72
+ end
26
73
 
27
- def test_nominal
28
- rate = Rate.new(0.0375, :effective)
29
- assert_equal D('0.03687'), rate.nominal.round(5)
74
+ should "raise an exception if an unknown value is given for :type" do
75
+ assert_raises(ArgumentError){ Rate.new(0.0375, :foo) }
76
+ end
30
77
  end
31
78
  end
metadata CHANGED
@@ -4,10 +4,10 @@ version: !ruby/object:Gem::Version
4
4
  hash: 23
5
5
  prerelease:
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 2
9
9
  - 0
10
- version: 0.2.0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Bill Kranec
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-28 00:00:00 Z
18
+ date: 2011-07-20 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: flt
@@ -41,19 +41,25 @@ extensions: []
41
41
 
42
42
  extra_rdoc_files:
43
43
  - README
44
+ - COPYING
45
+ - COPYING.LESSER
46
+ - HISTORY
44
47
  files:
45
48
  - README
46
49
  - COPYING
47
50
  - COPYING.LESSER
48
- - lib/amortization.rb
49
- - lib/cashflows.rb
51
+ - HISTORY
52
+ - lib/finance/amortization.rb
53
+ - lib/finance/cashflows.rb
54
+ - lib/finance/decimal.rb
55
+ - lib/finance/interval.rb
56
+ - lib/finance/rates.rb
57
+ - lib/finance/transaction.rb
50
58
  - lib/finance.rb
51
- - lib/rates.rb
52
- - lib/timespan.rb
53
59
  - test/test_amortization.rb
54
60
  - test/test_cashflows.rb
61
+ - test/test_interval.rb
55
62
  - test/test_rates.rb
56
- - test/test_timespan.rb
57
63
  homepage: https://rubygems.org/gems/finance
58
64
  licenses: []
59
65
 
@@ -67,10 +73,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
73
  requirements:
68
74
  - - ">="
69
75
  - !ruby/object:Gem::Version
70
- hash: 3
76
+ hash: 29
71
77
  segments:
72
- - 0
73
- version: "0"
78
+ - 1
79
+ - 9
80
+ version: "1.9"
74
81
  required_rubygems_version: !ruby/object:Gem::Requirement
75
82
  none: false
76
83
  requirements:
@@ -86,6 +93,6 @@ rubyforge_project:
86
93
  rubygems_version: 1.8.5
87
94
  signing_key:
88
95
  specification_version: 3
89
- summary: a library for financial calculations in Ruby.
96
+ summary: a library for financial modelling in Ruby.
90
97
  test_files: []
91
98
 
@@ -1,160 +0,0 @@
1
- require 'rubygems'
2
- require 'cashflows'
3
- require 'flt'
4
-
5
- class Period
6
- attr_accessor :payment
7
- attr_accessor :principal
8
- attr_accessor :rate
9
-
10
- def additional_payment
11
- @payment-@original_payment
12
- end
13
-
14
- # Return the remaining balance at the end of the period.
15
- def balance
16
- @principal + @payment + self.interest
17
- end
18
-
19
- def initialize(principal, rate, payment)
20
- @principal = principal
21
- @rate = rate
22
- @payment = payment
23
- @original_payment = payment
24
- end
25
-
26
- # Return the interest charged for the period.
27
- def interest
28
- (@principal * @rate).round(2)
29
- end
30
-
31
- def modify_payment(&modifier)
32
- value = modifier.call(self)
33
-
34
- # There's a chance that the block does not return a decimal.
35
- unless value.class == Flt::DecNum
36
- value = Flt::DecNum value.to_s
37
- end
38
-
39
- self.payment = value
40
- end
41
-
42
- def payment=(value)
43
- total = @principal + self.interest
44
- if value.abs > total
45
- @payment = -total
46
- else
47
- @payment = value
48
- end
49
- end
50
- end
51
-
52
- class Amortization
53
- attr_accessor :balance
54
- attr_accessor :block
55
- attr_accessor :rate_duration
56
- attr_accessor :payment
57
- attr_accessor :periods
58
- attr_accessor :principal
59
- attr_accessor :rates
60
-
61
- def ==(amortization)
62
- self.principal == amortization.principal and self.rates == amortization.rates and self.payments == amortization.payments
63
- end
64
-
65
- def additional_payments
66
- @periods.collect{ |period| period.additional_payment }
67
- end
68
-
69
- def amortize(rate)
70
- # For the purposes of calculating a payment, the relevant time
71
- # period is the remaining number of periods in the loan, _not_ the
72
- # duration of the rate itself.
73
- duration = @rate_duration - @periods.length
74
- payment = Amortization.payment @balance, rate.monthly, duration
75
-
76
- rate.duration.times do
77
- # Do this first in case the balance is zero already.
78
- if @balance.zero?
79
- break
80
- end
81
-
82
- period = Period.new(@balance, rate.monthly, payment)
83
-
84
- if @block
85
- period.modify_payment(&@block)
86
- end
87
-
88
- @periods << period
89
- @balance = period.balance
90
- end
91
-
92
- if @rates.length == 1
93
- @payment = self.payments[0]
94
- else
95
- @payment = nil
96
- end
97
- end
98
-
99
- # Compute the amortization of the principal.
100
- def compute
101
- @balance = @principal
102
- @periods = []
103
-
104
- @rates.each do |rate|
105
- amortize(rate)
106
- end
107
-
108
- # Add any remaining balance due to rounding error to the last payment.
109
- unless @balance.zero?
110
- @periods[-1].payment -= @balance
111
- @balance = 0
112
- end
113
- end
114
-
115
- def duration
116
- @periods.length
117
- end
118
-
119
- def initialize(principal, *rates, &block)
120
- @principal = principal
121
- @rates = rates
122
- @rate_duration = (rates.collect { |r| r.duration }).sum
123
- @block = block
124
-
125
- compute
126
- end
127
-
128
- def inspect
129
- "Amortization.new(#{@principal}"
130
- end
131
-
132
- # Return an Array with the amount of interest charged in each period.
133
- def interest
134
- @periods.collect { |period| period.interest }
135
- end
136
-
137
- # Return the periodic payment due on a loan, based on the
138
- #{http://en.wikipedia.org/wiki/Amortization_calculator amortization process}.
139
- def Amortization.payment(balance, rate, periods)
140
- -(balance * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
141
- end
142
-
143
- # Return an array with the payment amount for each period.
144
- def payments
145
- @periods.collect { |period| period.payment }
146
- end
147
-
148
- # "Pretty print" a text amortization table.
149
- def pprint
150
- @periods.each_with_index do |p, i|
151
- puts "%03d $%9s %8s $%7s $%7s $%9s" % [i, p.principal, p.rate, p.payment, p.interest, p.balance]
152
- end
153
- end
154
- end
155
-
156
- class Numeric
157
- def amortize(rate, &block)
158
- amortization = Amortization.new(self, rate, &block)
159
- end
160
- end
@@ -1,33 +0,0 @@
1
- module Cashflow
2
- # Return the {http://en.wikipedia.org/wiki/Internal_rate_of_return
3
- # internal rate of return} for a given sequence of cashflows.
4
- def irr(iterations=100)
5
- rate = 1.0
6
- investment = self[0]
7
- for i in 1..iterations+1
8
- rate = rate * (1 - self.npv(rate) / investment)
9
- end
10
- rate
11
- end
12
-
13
- # Return the {http://en.wikipedia.org/wiki/Net_present_value net present value} of a sequence of cash flows given
14
- # the discount rate _rate_.
15
- def npv(rate)
16
- total = 0.0
17
- self.each_with_index do |cashflow, index|
18
- total = total + cashflow / (1+rate) ** index
19
- end
20
- total
21
- end
22
-
23
- def sum
24
- self.inject(:+)
25
- end
26
-
27
- def xirr
28
- end
29
- end
30
-
31
- class Array
32
- include Cashflow
33
- end
@@ -1,93 +0,0 @@
1
- require 'rubygems'
2
- require 'flt'
3
-
4
- class Rate
5
- attr_accessor :duration
6
- attr_accessor :periods
7
- attr_accessor :nominal
8
-
9
- def ==(rate)
10
- self.nominal == rate.nominal and self.periods == rate.periods
11
- end
12
-
13
- # Alias method for *effective*.
14
- def apr=(apr)
15
- self.effective = apr
16
- end
17
-
18
- def apr
19
- self.effective
20
- end
21
-
22
- # Alias method for *effective*.
23
- def apy=(apy)
24
- self.effective = apy
25
- end
26
-
27
- def apy
28
- self.effective
29
- end
30
-
31
- def effective=(rate)
32
- unless @periods == Flt::DecNum.infinity
33
- @nominal = @periods * ((1 + rate) ** (1 / @periods) - 1)
34
- else
35
- @nominal = Math.log(rate + 1)
36
- end
37
- end
38
-
39
- def effective
40
- unless @periods == Flt::DecNum.infinity
41
- (1 + @nominal / @periods) ** @periods - 1
42
- else
43
- @nominal.exp - 1
44
- end
45
- end
46
-
47
- def initialize(rate, type, opts={})
48
- # Make sure the rate is a decimal.
49
- unless rate.class == Flt::DecNum
50
- rate = Flt::DecNum rate.to_s
51
- end
52
-
53
- # Set the compounding interval.
54
- compounding = opts.fetch(:compounds, :monthly)
55
-
56
- translate = {
57
- :annually => Flt::DecNum(1),
58
- :continuously => Flt::DecNum.infinity,
59
- :daily => Flt::DecNum(365),
60
- :monthly => Flt::DecNum(12),
61
- :quarterly => Flt::DecNum(4),
62
- :semiannually => Flt::DecNum(2)
63
- }
64
-
65
- if translate.has_key? compounding
66
- @periods = translate.fetch compounding
67
- elsif compounding.kind_of? Numeric
68
- @periods = Flt::DecNum compounding.to_s
69
- end
70
-
71
- # Set the rate in the proper way, based on the value of :type:.
72
- if %w{apr apy effective}.include? type.to_s
73
- self.effective = rate
74
- else
75
- @nominal = rate
76
- end
77
-
78
- # Set the remainder of the attributes provided in :opts:.
79
- opts.each do |key, value|
80
- unless key == :compounds
81
- send("#{key}=", value)
82
- end
83
- end
84
- end
85
-
86
- def inspect
87
- "Rate.new(#{self.apr.round(6)}, :apr)"
88
- end
89
-
90
- def monthly
91
- (self.effective / 12).round(15)
92
- end
93
- end