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