finance 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +2 -0
- data/COPYING.LESSER +2 -0
- data/HISTORY +26 -0
- data/README +24 -22
- data/lib/finance.rb +5 -53
- data/lib/finance/amortization.rb +196 -0
- data/lib/finance/cashflows.rb +55 -0
- data/lib/finance/decimal.rb +12 -0
- data/lib/finance/interval.rb +19 -0
- data/lib/finance/rates.rb +167 -0
- data/lib/finance/transaction.rb +119 -0
- data/test/test_amortization.rb +114 -114
- data/test/test_cashflows.rb +12 -10
- data/test/test_interval.rb +18 -0
- data/test/test_rates.rb +69 -22
- metadata +19 -12
- data/lib/amortization.rb +0 -160
- data/lib/cashflows.rb +0 -33
- data/lib/rates.rb +0 -93
- data/lib/timespan.rb +0 -10
- data/test/test_timespan.rb +0 -12
data/test/test_cashflows.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
-
|
2
|
-
require 'test/unit'
|
1
|
+
require_relative '../lib/finance/cashflows.rb'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
end
|
3
|
+
require 'flt/d'
|
4
|
+
require 'minitest/unit'
|
5
|
+
require 'shoulda'
|
9
6
|
|
10
|
-
class
|
11
|
-
|
12
|
-
|
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
|
data/test/test_rates.rb
CHANGED
@@ -1,31 +1,78 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative '../lib/finance/rates.rb'
|
2
|
+
include Finance
|
3
|
+
|
4
|
+
require 'flt'
|
3
5
|
require 'flt/d'
|
4
|
-
require '
|
6
|
+
require 'minitest/unit'
|
7
|
+
require 'shoulda'
|
5
8
|
|
6
9
|
class TestRates < Test::Unit::TestCase
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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.
|
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-
|
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
|
-
-
|
49
|
-
- lib/
|
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:
|
76
|
+
hash: 29
|
71
77
|
segments:
|
72
|
-
-
|
73
|
-
|
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
|
96
|
+
summary: a library for financial modelling in Ruby.
|
90
97
|
test_files: []
|
91
98
|
|
data/lib/amortization.rb
DELETED
@@ -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
|
data/lib/cashflows.rb
DELETED
@@ -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
|
data/lib/rates.rb
DELETED
@@ -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
|